@sideboard-ai/core 0.1.157 → 0.1.158

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.
@@ -24,7 +24,7 @@ import {
24
24
  stripBrightsyNdjsonNoise,
25
25
  sumUsageList,
26
26
  toolDescription
27
- } from "./chunk-TIJ6QVX5.js";
27
+ } from "./chunk-BLRHO7XQ.js";
28
28
  import {
29
29
  SLACK_REPLY_FORMATTING,
30
30
  addWorkspace,
@@ -101,7 +101,9 @@ import {
101
101
  } from "./chunk-LQMFCS54.js";
102
102
  import {
103
103
  ATTACHMENTS_DIR,
104
- LEGACY_ATTACHMENTS_DIR
104
+ DETACHED_JOBS_DIR,
105
+ LEGACY_ATTACHMENTS_DIR,
106
+ LEGACY_DETACHED_JOBS_DIR
105
107
  } from "./chunk-JRH62XC4.js";
106
108
  import {
107
109
  httpFetch
@@ -459,7 +461,7 @@ async function continueSourceThread(threadId, prompt) {
459
461
  await continueOnReply(threadId, prompt);
460
462
  return;
461
463
  }
462
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-BUH3LJLL.js");
464
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-ZVW2AC6B.js");
463
465
  await getOrchestrator2().send(threadId, prompt);
464
466
  } catch {
465
467
  }
@@ -719,7 +721,7 @@ async function pollSlackOutboundWatches(opts) {
719
721
  }
720
722
 
721
723
  // src/orchestrator/orchestrator.ts
722
- import { existsSync as existsSync17 } from "fs";
724
+ import { existsSync as existsSync18 } from "fs";
723
725
 
724
726
  // src/agents/spawn.ts
725
727
  import { createInterface } from "readline";
@@ -2564,8 +2566,232 @@ function notifyParentOfChildHalt(child, status, send) {
2564
2566
  return true;
2565
2567
  }
2566
2568
 
2569
+ // src/mcp/wait-for-job.ts
2570
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2571
+ import { dirname as dirname3, join as join10 } from "path";
2572
+
2573
+ // src/mcp/wait-for-turn.ts
2574
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
2575
+ function mcpWaitForTurnTimeoutMs(requested) {
2576
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
2577
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
2578
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
2579
+ }
2580
+ 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.";
2581
+ var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume it failed to start.";
2582
+ function mcpWaitStillRunningHint(status) {
2583
+ return status === "queued" ? MCP_WAIT_QUEUED_HINT : MCP_WAIT_STILL_RUNNING_HINT;
2584
+ }
2585
+ var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
2586
+ var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
2587
+ var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
2588
+ function mcpWaitFinishedHint(status) {
2589
+ if (status === "stopped") return MCP_WAIT_STOPPED_HINT;
2590
+ if (status === "broken") return MCP_WAIT_BROKEN_HINT;
2591
+ if (status === "error") return MCP_WAIT_ERROR_HINT;
2592
+ return void 0;
2593
+ }
2594
+
2595
+ // src/mcp/wait-for-job.ts
2596
+ var MAX_JOB_CONTINUES = 8;
2597
+ var MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. present_artifact type=log with the same artifact_id and content=delta (new lines only). Then call wait_for_job again. Do not end the turn or tell the user you will let them know later.";
2598
+ var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
2599
+ function mcpWaitForJobTimeoutMs(requested) {
2600
+ return mcpWaitForTurnTimeoutMs(requested);
2601
+ }
2602
+ function sanitizeDetachedJobId(id) {
2603
+ const s = id.trim();
2604
+ if (!JOB_ID_RE.test(s)) {
2605
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
2606
+ }
2607
+ return s;
2608
+ }
2609
+ function jobAlive(pid) {
2610
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2611
+ try {
2612
+ process.kill(pid, 0);
2613
+ return true;
2614
+ } catch {
2615
+ return false;
2616
+ }
2617
+ }
2618
+ function readIntFile(file) {
2619
+ if (!existsSync8(file)) return null;
2620
+ const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
2621
+ return Number.isInteger(n) ? n : null;
2622
+ }
2623
+ function jobDir(root, id, legacy = false) {
2624
+ return join10(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
2625
+ }
2626
+ function resolveJobDir(root, id) {
2627
+ const modern = jobDir(root, id, false);
2628
+ if (existsSync8(modern)) return modern;
2629
+ const legacy = jobDir(root, id, true);
2630
+ if (existsSync8(legacy)) return legacy;
2631
+ return modern;
2632
+ }
2633
+ function listJobIdsIn(root, rel) {
2634
+ const dir = join10(root, rel);
2635
+ if (!existsSync8(dir)) return [];
2636
+ return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
2637
+ }
2638
+ function listRunningDetachedJobs(worktreePath) {
2639
+ const root = worktreePath.trim();
2640
+ if (!root) return [];
2641
+ const ids = /* @__PURE__ */ new Set([
2642
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
2643
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
2644
+ ]);
2645
+ const running = [];
2646
+ for (const id of ids) {
2647
+ const dir = resolveJobDir(root, id);
2648
+ const pid = readIntFile(join10(dir, "pid"));
2649
+ if (pid != null && jobAlive(pid)) running.push(id);
2650
+ }
2651
+ return running.sort();
2652
+ }
2653
+ function looksLikeDeferredDonePromise(text) {
2654
+ const t = (text ?? "").trim();
2655
+ if (!t) return false;
2656
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(t) || /\blet you know when\b/i.test(t) || /\b(check back|ping me)\s+when\b/i.test(t) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(t);
2657
+ }
2658
+ function formatJobStillRunningContinuePrompt(jobIds) {
2659
+ const ids = jobIds.join(", ");
2660
+ return [
2661
+ `Detached job still running: ${ids}.`,
2662
+ "Do not end this turn. Loop wait_for_job (same id) and present_artifact type=log with content=delta until stillRunning is false.",
2663
+ "Then report the result. Do not tell the user you will let them know later."
2664
+ ].join(" ");
2665
+ }
2666
+ function formatDeferredDoneContinuePrompt() {
2667
+ return [
2668
+ "You ended the turn after promising to report later, but no detached job is running.",
2669
+ "If tests/pack/deploy still need to run: start once with detached-job.js, present_artifact type=log, then loop wait_for_job until stillRunning is false.",
2670
+ "Do not say you will let the user know later."
2671
+ ].join(" ");
2672
+ }
2673
+ function turnWatchedDetachedJob(parts) {
2674
+ return parts.some((p) => {
2675
+ if (p.type !== "tool") return false;
2676
+ if (/wait_for_job$/i.test(p.name ?? "")) return true;
2677
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
2678
+ return /detached-job\.js\b/i.test(blob);
2679
+ });
2680
+ }
2681
+ function planJobContinue(opts) {
2682
+ if (opts.isOrchestrator) return { action: "none" };
2683
+ if (opts.agent === "brightsy") return { action: "none" };
2684
+ if (opts.queueLength > 0) return { action: "none" };
2685
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
2686
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
2687
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
2688
+ return {
2689
+ action: "wait",
2690
+ jobIds: opts.runningJobIds,
2691
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
2692
+ };
2693
+ }
2694
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
2695
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
2696
+ }
2697
+ return { action: "none" };
2698
+ }
2699
+ function tailProgress(logFile, maxLines = 12) {
2700
+ if (!existsSync8(logFile)) return "(no log yet)";
2701
+ const lines = readFileSync7(logFile, "utf8").split("\n");
2702
+ return lines.slice(-maxLines).join("\n");
2703
+ }
2704
+ function readLogLines(file) {
2705
+ if (!existsSync8(file)) return [];
2706
+ const lines = readFileSync7(file, "utf8").split("\n");
2707
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
2708
+ return lines;
2709
+ }
2710
+ function takeDelta(logFile, cursorFile) {
2711
+ const lines = readLogLines(logFile);
2712
+ const cursor = readIntFile(cursorFile) ?? 0;
2713
+ const start = Math.min(Math.max(0, cursor), lines.length);
2714
+ return { delta: lines.slice(start).join("\n"), nextCursor: lines.length };
2715
+ }
2716
+ function snapshotJob(dir) {
2717
+ const pid = readIntFile(join10(dir, "pid"));
2718
+ const running = pid != null && jobAlive(pid);
2719
+ return {
2720
+ pid,
2721
+ running,
2722
+ exitCode: readIntFile(join10(dir, "exit")),
2723
+ log: join10(dir, "log"),
2724
+ cursor: join10(dir, "present.cursor"),
2725
+ progress: tailProgress(join10(dir, "log"))
2726
+ };
2727
+ }
2728
+ function toResult(id, snap, extra) {
2729
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
2730
+ const ok = !snap.running && snap.exitCode === 0;
2731
+ const stillRunning = snap.running && !ok;
2732
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
2733
+ try {
2734
+ mkdirSync3(dirname3(snap.cursor), { recursive: true });
2735
+ writeFileSync4(snap.cursor, `${nextCursor}
2736
+ `);
2737
+ } catch {
2738
+ }
2739
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
2740
+ return {
2741
+ stillRunning,
2742
+ ok,
2743
+ failed: Boolean(failed && !stillRunning && !ok),
2744
+ status,
2745
+ id,
2746
+ pid: snap.pid,
2747
+ exitCode: snap.exitCode ?? void 0,
2748
+ delta,
2749
+ progress: extra?.progress ?? snap.progress,
2750
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
2751
+ };
2752
+ }
2753
+ async function sleepMs(ms) {
2754
+ await new Promise((resolve) => setTimeout(resolve, ms));
2755
+ }
2756
+ async function waitForDetachedJob(cwd, id, opts) {
2757
+ const jobId = sanitizeDetachedJobId(id);
2758
+ const root = cwd.trim() || process.cwd();
2759
+ const dir = resolveJobDir(root, jobId);
2760
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
2761
+ const sleep = opts?.sleep ?? sleepMs;
2762
+ if (!existsSync8(dir)) {
2763
+ return {
2764
+ stillRunning: false,
2765
+ ok: false,
2766
+ failed: true,
2767
+ status: "failed",
2768
+ id: jobId,
2769
+ delta: "",
2770
+ progress: "No detached job. Start one first.",
2771
+ hint: "Start with detached-job.js start <id> -- <command>, then call wait_for_job again."
2772
+ };
2773
+ }
2774
+ const deadline = Date.now() + timeoutMs;
2775
+ let snap = snapshotJob(dir);
2776
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
2777
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
2778
+ return toResult(jobId, snap, {
2779
+ failed: true,
2780
+ progress: "No detached job. Start one first."
2781
+ });
2782
+ }
2783
+ while (Date.now() < deadline) {
2784
+ snap = snapshotJob(dir);
2785
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
2786
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
2787
+ await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
2788
+ }
2789
+ snap = snapshotJob(dir);
2790
+ return toResult(jobId, snap);
2791
+ }
2792
+
2567
2793
  // src/threads/create.ts
2568
- import { existsSync as existsSync8 } from "fs";
2794
+ import { existsSync as existsSync9 } from "fs";
2569
2795
 
2570
2796
  // src/detect/detect.ts
2571
2797
  async function detectAgents() {
@@ -3127,7 +3353,7 @@ function reuseLiveThread(input, repoPath, match) {
3127
3353
  }
3128
3354
  async function createThread(input, _onSetupLine) {
3129
3355
  const repoPath = await resolveRepoRoot(input.repoPath);
3130
- if (!existsSync8(repoPath)) {
3356
+ if (!existsSync9(repoPath)) {
3131
3357
  throw new Error(`Repo not found: ${repoPath}`);
3132
3358
  }
3133
3359
  const reused = reuseLiveThread(input, repoPath, {
@@ -3299,7 +3525,7 @@ async function createThread(input, _onSetupLine) {
3299
3525
  return readThread(thread.id) ?? thread;
3300
3526
  }
3301
3527
  async function listLinearIssues(agent, repoPath) {
3302
- const { getAdapter: getAdapter2 } = await import("./agents-LCDXN2B2.js");
3528
+ const { getAdapter: getAdapter2 } = await import("./agents-E3ZPWZRS.js");
3303
3529
  await requireAgent(agent, { requireLinear: true });
3304
3530
  const adapter = getAdapter2(agent);
3305
3531
  if (!adapter.listLinearIssues) {
@@ -3322,11 +3548,11 @@ function shouldRemoveWorktreeOnTeardown(thread) {
3322
3548
 
3323
3549
  // src/store/turn-live.ts
3324
3550
  import {
3325
- existsSync as existsSync9,
3551
+ existsSync as existsSync10,
3326
3552
  renameSync,
3327
3553
  unlinkSync as unlinkSync2,
3328
- writeFileSync as writeFileSync4,
3329
- readFileSync as readFileSync7
3554
+ writeFileSync as writeFileSync5,
3555
+ readFileSync as readFileSync8
3330
3556
  } from "fs";
3331
3557
  var buffers = /* @__PURE__ */ new Map();
3332
3558
  var FLUSH_MS = 800;
@@ -3403,7 +3629,7 @@ function writeTurnLive(threadId, progress) {
3403
3629
  const path = threadLivePath(threadId);
3404
3630
  const tmp = `${path}.${process.pid}.tmp`;
3405
3631
  try {
3406
- writeFileSync4(tmp, JSON.stringify(progress), "utf8");
3632
+ writeFileSync5(tmp, JSON.stringify(progress), "utf8");
3407
3633
  renameSync(tmp, path);
3408
3634
  } catch {
3409
3635
  try {
@@ -3414,9 +3640,9 @@ function writeTurnLive(threadId, progress) {
3414
3640
  }
3415
3641
  function readTurnLive(threadId) {
3416
3642
  const path = threadLivePath(threadId);
3417
- if (!existsSync9(path)) return null;
3643
+ if (!existsSync10(path)) return null;
3418
3644
  try {
3419
- const raw = JSON.parse(readFileSync7(path, "utf8"));
3645
+ const raw = JSON.parse(readFileSync8(path, "utf8"));
3420
3646
  if (!raw || typeof raw.summary !== "string") return null;
3421
3647
  return raw;
3422
3648
  } catch {
@@ -3428,7 +3654,7 @@ function clearTurnLive(threadId) {
3428
3654
  if (buf?.timer) clearTimeout(buf.timer);
3429
3655
  buffers.delete(threadId);
3430
3656
  const path = threadLivePath(threadId);
3431
- if (!existsSync9(path)) return;
3657
+ if (!existsSync10(path)) return;
3432
3658
  try {
3433
3659
  unlinkSync2(path);
3434
3660
  } catch {
@@ -3616,26 +3842,26 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
3616
3842
  import { execFileSync } from "child_process";
3617
3843
  import {
3618
3844
  copyFileSync as copyFileSync2,
3619
- existsSync as existsSync10,
3845
+ existsSync as existsSync11,
3620
3846
  mkdtempSync,
3621
- readdirSync as readdirSync3,
3622
- readFileSync as readFileSync8,
3847
+ readdirSync as readdirSync4,
3848
+ readFileSync as readFileSync9,
3623
3849
  rmSync
3624
3850
  } from "fs";
3625
3851
  import { tmpdir } from "os";
3626
- import { join as join10 } from "path";
3852
+ import { join as join11 } from "path";
3627
3853
  import { createRequire } from "module";
3628
- var CONDUCTOR_APP_SUPPORT = join10(
3854
+ var CONDUCTOR_APP_SUPPORT = join11(
3629
3855
  process.env.HOME ?? "",
3630
3856
  "Library",
3631
3857
  "Application Support",
3632
3858
  "com.conductor.app"
3633
3859
  );
3634
- var CONDUCTOR_DB = join10(CONDUCTOR_APP_SUPPORT, "conductor.db");
3635
- var CURSOR_SDK_STORE = join10(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
3860
+ var CONDUCTOR_DB = join11(CONDUCTOR_APP_SUPPORT, "conductor.db");
3861
+ var CURSOR_SDK_STORE = join11(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
3636
3862
  function thisModuleFile() {
3637
3863
  const cjsFile = typeof __filename !== "undefined" ? __filename : "";
3638
- return cjsFile || process.argv[1] || join10(process.cwd(), "package.json");
3864
+ return cjsFile || process.argv[1] || join11(process.cwd(), "package.json");
3639
3865
  }
3640
3866
  function openReadonlySqlite(file) {
3641
3867
  const req = createRequire(thisModuleFile());
@@ -3653,21 +3879,21 @@ function mapAgentType(raw) {
3653
3879
  return null;
3654
3880
  }
3655
3881
  function resolveConductorCursorAgentId(workspacePath) {
3656
- if (!workspacePath || !existsSync10(CURSOR_SDK_STORE)) return null;
3882
+ if (!workspacePath || !existsSync11(CURSOR_SDK_STORE)) return null;
3657
3883
  const normalized = workspacePath.replace(/\/$/, "");
3658
3884
  let best = null;
3659
3885
  let hashes;
3660
3886
  try {
3661
- hashes = readdirSync3(CURSOR_SDK_STORE);
3887
+ hashes = readdirSync4(CURSOR_SDK_STORE);
3662
3888
  } catch {
3663
3889
  return null;
3664
3890
  }
3665
3891
  for (const hash of hashes) {
3666
- const agentsFile = join10(CURSOR_SDK_STORE, hash, "agents.ndjson");
3667
- if (!existsSync10(agentsFile)) continue;
3892
+ const agentsFile = join11(CURSOR_SDK_STORE, hash, "agents.ndjson");
3893
+ if (!existsSync11(agentsFile)) continue;
3668
3894
  let text;
3669
3895
  try {
3670
- text = readFileSync8(agentsFile, "utf8");
3896
+ text = readFileSync9(agentsFile, "utf8");
3671
3897
  } catch {
3672
3898
  continue;
3673
3899
  }
@@ -3691,7 +3917,7 @@ function resolveConductorCursorAgentId(workspacePath) {
3691
3917
  return best?.agentId ?? null;
3692
3918
  }
3693
3919
  async function adoptThread(input) {
3694
- if (!existsSync10(input.worktreePath)) {
3920
+ if (!existsSync11(input.worktreePath)) {
3695
3921
  throw new Error(`Worktree not found: ${input.worktreePath}`);
3696
3922
  }
3697
3923
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -3720,16 +3946,16 @@ function conductorDbPath() {
3720
3946
  return CONDUCTOR_DB;
3721
3947
  }
3722
3948
  function listConductorWorkspaces() {
3723
- if (!existsSync10(CONDUCTOR_DB)) {
3949
+ if (!existsSync11(CONDUCTOR_DB)) {
3724
3950
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
3725
3951
  }
3726
- const tmp = mkdtempSync(join10(tmpdir(), "sideboard-conductor-"));
3727
- const snapshot = join10(tmp, "conductor.db");
3952
+ const tmp = mkdtempSync(join11(tmpdir(), "sideboard-conductor-"));
3953
+ const snapshot = join11(tmp, "conductor.db");
3728
3954
  try {
3729
3955
  copyFileSync2(CONDUCTOR_DB, snapshot);
3730
3956
  for (const suffix of ["-wal", "-shm"]) {
3731
3957
  const src = `${CONDUCTOR_DB}${suffix}`;
3732
- if (existsSync10(src)) {
3958
+ if (existsSync11(src)) {
3733
3959
  try {
3734
3960
  copyFileSync2(src, `${snapshot}${suffix}`);
3735
3961
  } catch {
@@ -3811,16 +4037,16 @@ function listConductorWorkspaces() {
3811
4037
  }
3812
4038
  }
3813
4039
  function importConductorWorkspace(workspaceId) {
3814
- if (!existsSync10(CONDUCTOR_DB)) {
4040
+ if (!existsSync11(CONDUCTOR_DB)) {
3815
4041
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
3816
4042
  }
3817
- const tmp = mkdtempSync(join10(tmpdir(), "sideboard-conductor-"));
3818
- const snapshot = join10(tmp, "conductor.db");
4043
+ const tmp = mkdtempSync(join11(tmpdir(), "sideboard-conductor-"));
4044
+ const snapshot = join11(tmp, "conductor.db");
3819
4045
  try {
3820
4046
  copyFileSync2(CONDUCTOR_DB, snapshot);
3821
4047
  for (const suffix of ["-wal", "-shm"]) {
3822
4048
  const src = `${CONDUCTOR_DB}${suffix}`;
3823
- if (existsSync10(src)) {
4049
+ if (existsSync11(src)) {
3824
4050
  try {
3825
4051
  copyFileSync2(src, `${snapshot}${suffix}`);
3826
4052
  } catch {
@@ -3840,7 +4066,7 @@ function importConductorWorkspace(workspaceId) {
3840
4066
  ).get(workspaceId);
3841
4067
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
3842
4068
  const worktreePath = String(row.workspacePath);
3843
- if (!existsSync10(worktreePath)) {
4069
+ if (!existsSync11(worktreePath)) {
3844
4070
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
3845
4071
  }
3846
4072
  let sessionId = null;
@@ -3911,7 +4137,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
3911
4137
  }
3912
4138
 
3913
4139
  // src/threads/stack-layers.ts
3914
- import { existsSync as existsSync11 } from "fs";
4140
+ import { existsSync as existsSync12 } from "fs";
3915
4141
  function stackIdFrom(stack) {
3916
4142
  if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
3917
4143
  const key = stack.layers.map((l) => l.branchName).join("|");
@@ -3974,7 +4200,7 @@ async function openStackLayer(input, _onSetupLine) {
3974
4200
  let createdWorktree = false;
3975
4201
  const trees = await listWorktrees(repoPath);
3976
4202
  const checkedOut = trees.find((w) => w.branch === branchName);
3977
- if (checkedOut?.path && existsSync11(checkedOut.path)) {
4203
+ if (checkedOut?.path && existsSync12(checkedOut.path)) {
3978
4204
  if (input.reuseExistingWorktree !== false) {
3979
4205
  worktreePath = checkedOut.path;
3980
4206
  } else {
@@ -4116,7 +4342,7 @@ async function initStackFromThread(input, onSetupLine) {
4116
4342
  async function createPrStack(input, onSetupLine) {
4117
4343
  await requireAgent(input.agent);
4118
4344
  const repoPath = await resolveRepoRoot(input.repoPath);
4119
- if (!existsSync11(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
4345
+ if (!existsSync12(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
4120
4346
  if (!input.branches.length) throw new Error("At least one branch name required");
4121
4347
  const status = await detectGhStack(repoPath);
4122
4348
  if (!status.available) throw new Error(status.reason);
@@ -4183,7 +4409,7 @@ async function createPrStack(input, onSetupLine) {
4183
4409
  }
4184
4410
  }
4185
4411
  const claimed = new Set(threads.map((t) => t.worktreePath));
4186
- if (!claimed.has(bootstrap.worktreePath) && existsSync11(bootstrap.worktreePath)) {
4412
+ if (!claimed.has(bootstrap.worktreePath) && existsSync12(bootstrap.worktreePath)) {
4187
4413
  try {
4188
4414
  await removeWorktree(repoPath, bootstrap.worktreePath, {
4189
4415
  deleteBranch: bootstrap.branchName
@@ -4205,10 +4431,10 @@ function stackAgentDefaultsFrom(input) {
4205
4431
  }
4206
4432
 
4207
4433
  // src/diff/diff.ts
4208
- import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
4209
- import { dirname as dirname3, join as join11 } from "path";
4434
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
4435
+ import { dirname as dirname4, join as join12 } from "path";
4210
4436
  async function inspectGitWorktree(worktreePath) {
4211
- if (!worktreePath || !existsSync12(worktreePath)) return "missing_worktree";
4437
+ if (!worktreePath || !existsSync13(worktreePath)) return "missing_worktree";
4212
4438
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
4213
4439
  reject: false
4214
4440
  });
@@ -4216,7 +4442,7 @@ async function inspectGitWorktree(worktreePath) {
4216
4442
  return "ok";
4217
4443
  }
4218
4444
  async function initializeGitRepository(worktreePath) {
4219
- if (!worktreePath || !existsSync12(worktreePath)) {
4445
+ if (!worktreePath || !existsSync13(worktreePath)) {
4220
4446
  throw new Error("Worktree not found");
4221
4447
  }
4222
4448
  const status = await inspectGitWorktree(worktreePath);
@@ -4352,11 +4578,11 @@ new file mode 100644
4352
4578
  };
4353
4579
  }
4354
4580
  async function untrackedPatch(worktreePath, path, maxHunk) {
4355
- const abs = join11(worktreePath, path);
4581
+ const abs = join12(worktreePath, path);
4356
4582
  try {
4357
4583
  const st = statSync2(abs);
4358
4584
  if (st.isFile() && st.size > maxHunk) {
4359
- const buf = readFileSync9(abs).subarray(0, maxHunk);
4585
+ const buf = readFileSync10(abs).subarray(0, maxHunk);
4360
4586
  return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
4361
4587
  }
4362
4588
  } catch {
@@ -4857,7 +5083,7 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
4857
5083
  function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
4858
5084
  assertSafeRelativePath(relativePath);
4859
5085
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
4860
- const abs = join11(worktreePath, relativePath);
5086
+ const abs = join12(worktreePath, relativePath);
4861
5087
  const st = statSync2(abs);
4862
5088
  if (!st.isFile()) {
4863
5089
  throw new Error(`Not a file: ${relativePath}`);
@@ -4867,7 +5093,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
4867
5093
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
4868
5094
  );
4869
5095
  }
4870
- const buf = readFileSync9(abs);
5096
+ const buf = readFileSync10(abs);
4871
5097
  return {
4872
5098
  path: relativePath,
4873
5099
  contentBase64: buf.toString("base64"),
@@ -4877,12 +5103,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
4877
5103
  function readWorktreeFile(worktreePath, relativePath, opts) {
4878
5104
  assertSafeRelativePath(relativePath);
4879
5105
  const maxBytes = opts?.maxBytes ?? 2e5;
4880
- const abs = join11(worktreePath, relativePath);
5106
+ const abs = join12(worktreePath, relativePath);
4881
5107
  const st = statSync2(abs);
4882
5108
  if (!st.isFile()) {
4883
5109
  throw new Error(`Not a file: ${relativePath}`);
4884
5110
  }
4885
- const buf = readFileSync9(abs);
5111
+ const buf = readFileSync10(abs);
4886
5112
  if (isImageRelativePath(relativePath)) {
4887
5113
  const maxImageBytes = Math.max(maxBytes, 15e6);
4888
5114
  const truncated2 = buf.length > maxImageBytes;
@@ -4925,9 +5151,9 @@ function assertSafeRelativePath(relativePath) {
4925
5151
  }
4926
5152
  function writeWorktreeFile(worktreePath, relativePath, content) {
4927
5153
  assertSafeRelativePath(relativePath);
4928
- const abs = join11(worktreePath, relativePath);
4929
- mkdirSync3(dirname3(abs), { recursive: true });
4930
- writeFileSync5(abs, content, "utf8");
5154
+ const abs = join12(worktreePath, relativePath);
5155
+ mkdirSync4(dirname4(abs), { recursive: true });
5156
+ writeFileSync6(abs, content, "utf8");
4931
5157
  return { path: relativePath };
4932
5158
  }
4933
5159
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -5052,9 +5278,9 @@ async function confirmLand(thread, opts) {
5052
5278
  }
5053
5279
 
5054
5280
  // src/skills/discover.ts
5055
- import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
5281
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
5056
5282
  import { homedir } from "os";
5057
- import { join as join12 } from "path";
5283
+ import { join as join13 } from "path";
5058
5284
 
5059
5285
  // src/skills/bundled/long-running.ts
5060
5286
  var LONG_RUNNING_SKILL_COMMAND = "long-running";
@@ -5065,7 +5291,7 @@ var LONG_RUNNING_SKILL_BODY = `# Long-running jobs
5065
5291
 
5066
5292
  A Sideboard or Cursor **worktree turn** SIGTERMs the agent shell (and its process group) when the user sends another message or the turn is interrupted. \`block_until_ms: 0\` is not enough \u2014 the child stays in that group.
5067
5293
 
5068
- Do **not** ask the human to check back. Detach, then **wait** in 45s slices (same idea as MCP \`wait_for_turn\`) until \`stillRunning\` is false.
5294
+ Do **not** ask the human to check back. Detach, then **wait** in 45s slices (MCP \`wait_for_job\`, same idea as \`wait_for_turn\`) until \`stillRunning\` is false. Do not end the turn with \u201CI\u2019ll let you know.\u201D
5069
5295
 
5070
5296
  ## Tool
5071
5297
 
@@ -5075,7 +5301,8 @@ Use the helper from the Sideboard playbook \u2014 an absolute \`node "\u2026" st
5075
5301
  # Start (exits in ~1s; job survives this turn)
5076
5302
  node <detached-job.js> start <id> -- <command> [args...]
5077
5303
 
5078
- # Wait \u2014 returns within ~45s even if the job is still going
5304
+ # Wait \u2014 prefer MCP wait_for_job (same 45s / stillRunning contract).
5305
+ # Shell fallback:
5079
5306
  node <detached-job.js> wait <id>
5080
5307
 
5081
5308
  # Block until the process exits (humans / a turn that will not be interrupted)
@@ -5088,7 +5315,7 @@ node <detached-job.js> status <id>
5088
5315
 
5089
5316
  Wait JSON:
5090
5317
 
5091
- - \`stillRunning: true\` \u2192 exit 2 \u2192 **call wait again**. Progress is in \`progress\` / \`phase\`. Do not start a second job. Do not ping the user.
5318
+ - \`stillRunning: true\` \u2192 **call wait_for_job again** (or shell wait). Progress is in \`progress\` / \`phase\`. Do not start a second job. Do not ping the user.
5092
5319
  - \`ok: true\` \u2192 exit 0 \u2192 continue the rest of the task.
5093
5320
  - \`failed: true\` \u2192 exit 1 \u2192 read \`progress\`, fix, start **once**.
5094
5321
 
@@ -5118,7 +5345,7 @@ node <detached-job.js> wait --pid-file FILE --log-file FILE [--ok-pattern TEXT]
5118
5345
 
5119
5346
  1. \`start\` once. If JSON says \`already-running\`, do not start again.
5120
5347
  2. Immediately \`present_artifact\` \`type=log\` (same \`artifact_id\`, \`status=running\`) \u2014 the human should see **working** in the side column, not a \u201Ccheck back later\u201D message.
5121
- 3. Loop \`wait\` (use \`--timeout-ms 15000\` for a livelier pane). After each slice, \`present_artifact\` the same id with \`content=delta\` only.
5348
+ 3. Loop \`wait_for_job\` (or shell \`wait\`). After each slice, \`present_artifact\` the same id with \`content=delta\` only.
5122
5349
  4. On \`ok\`, present once more (\`status=ok\`, last \`delta\`) and finish the task. On \`failed\`, fix from the log.
5123
5350
 
5124
5351
  Never tell the user \u201Csay status when it\u2019s done.\u201D You wait.
@@ -5183,7 +5410,7 @@ function parseFrontmatter(content) {
5183
5410
  }
5184
5411
  function readSkill(skillMd, source) {
5185
5412
  try {
5186
- const content = readFileSync10(skillMd, "utf8");
5413
+ const content = readFileSync11(skillMd, "utf8");
5187
5414
  const { name: fmName, description } = parseFrontmatter(content);
5188
5415
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
5189
5416
  const name = fmName || dirName;
@@ -5202,17 +5429,17 @@ function readSkill(skillMd, source) {
5202
5429
  }
5203
5430
  }
5204
5431
  function scanSkillsDir(dir, source, out) {
5205
- if (!existsSync13(dir)) return;
5432
+ if (!existsSync14(dir)) return;
5206
5433
  let entries;
5207
5434
  try {
5208
- entries = readdirSync4(dir);
5435
+ entries = readdirSync5(dir);
5209
5436
  } catch {
5210
5437
  return;
5211
5438
  }
5212
5439
  for (const entry of entries) {
5213
5440
  if (entry.startsWith(".")) continue;
5214
- const skillMd = join12(dir, entry, "SKILL.md");
5215
- if (!existsSync13(skillMd)) continue;
5441
+ const skillMd = join13(dir, entry, "SKILL.md");
5442
+ if (!existsSync14(skillMd)) continue;
5216
5443
  try {
5217
5444
  if (!statSync3(skillMd).isFile()) continue;
5218
5445
  } catch {
@@ -5223,22 +5450,22 @@ function scanSkillsDir(dir, source, out) {
5223
5450
  }
5224
5451
  }
5225
5452
  function scanClaudePluginSkills(pluginsRoot, out) {
5226
- if (!existsSync13(pluginsRoot)) return;
5453
+ if (!existsSync14(pluginsRoot)) return;
5227
5454
  const walk = (dir, depth, lookingForSkillsDir) => {
5228
5455
  if (depth > 7) return;
5229
5456
  let entries;
5230
5457
  try {
5231
- entries = readdirSync4(dir);
5458
+ entries = readdirSync5(dir);
5232
5459
  } catch {
5233
5460
  return;
5234
5461
  }
5235
5462
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
5236
- const skill = readSkill(join12(dir, "SKILL.md"), "cli");
5463
+ const skill = readSkill(join13(dir, "SKILL.md"), "cli");
5237
5464
  if (skill) out.push(skill);
5238
5465
  }
5239
5466
  for (const entry of entries) {
5240
5467
  if (entry === "node_modules" || entry === ".git") continue;
5241
- const full = join12(dir, entry);
5468
+ const full = join13(dir, entry);
5242
5469
  try {
5243
5470
  if (!statSync3(full).isDirectory()) continue;
5244
5471
  } catch {
@@ -5258,17 +5485,17 @@ function discoverSkills(worktreePath) {
5258
5485
  const home = homedir();
5259
5486
  const collected = [];
5260
5487
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
5261
- scanSkillsDir(join12(worktreePath, rel), "workspace", collected);
5488
+ scanSkillsDir(join13(worktreePath, rel), "workspace", collected);
5262
5489
  }
5263
5490
  for (const abs of [
5264
- join12(home, ".claude/skills"),
5265
- join12(home, ".cursor/skills"),
5266
- join12(home, ".sideboard/skills"),
5267
- join12(home, ".brightsy/skills")
5491
+ join13(home, ".claude/skills"),
5492
+ join13(home, ".cursor/skills"),
5493
+ join13(home, ".sideboard/skills"),
5494
+ join13(home, ".brightsy/skills")
5268
5495
  ]) {
5269
5496
  scanSkillsDir(abs, "user", collected);
5270
5497
  }
5271
- scanClaudePluginSkills(join12(home, ".claude/plugins"), collected);
5498
+ scanClaudePluginSkills(join13(home, ".claude/plugins"), collected);
5272
5499
  collected.push(...bundledSkills());
5273
5500
  const rank = {
5274
5501
  workspace: 0,
@@ -5294,7 +5521,7 @@ function readSkillBody(skillPath, maxChars = 12e3) {
5294
5521
  \u2026(truncated)` : body;
5295
5522
  }
5296
5523
  }
5297
- const raw = readFileSync10(skillPath, "utf8");
5524
+ const raw = readFileSync11(skillPath, "utf8");
5298
5525
  if (raw.startsWith("---")) {
5299
5526
  const end = raw.indexOf("\n---", 3);
5300
5527
  if (end >= 0) {
@@ -5387,27 +5614,27 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
5387
5614
  }
5388
5615
 
5389
5616
  // src/agents/instructions.ts
5390
- import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
5391
- import { join as join14 } from "path";
5617
+ import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
5618
+ import { join as join15 } from "path";
5392
5619
 
5393
5620
  // src/skills/detached-job-path.ts
5394
- import { existsSync as existsSync14 } from "fs";
5395
- import { dirname as dirname4, join as join13 } from "path";
5621
+ import { existsSync as existsSync15 } from "fs";
5622
+ import { dirname as dirname5, join as join14 } from "path";
5396
5623
  import { fileURLToPath } from "url";
5397
5624
  function packagedDetachedJobPath() {
5398
5625
  const dir = packagedMcpDir();
5399
5626
  if (!dir) return null;
5400
- const script = join13(dir, "scripts", "detached-job.js");
5401
- return existsSync14(script) ? script : null;
5627
+ const script = join14(dir, "scripts", "detached-job.js");
5628
+ return existsSync15(script) ? script : null;
5402
5629
  }
5403
5630
  function resolveDetachedJobScript() {
5404
5631
  const packaged = packagedDetachedJobPath();
5405
5632
  if (packaged) return packaged;
5406
- let dir = dirname4(fileURLToPath(import.meta.url));
5633
+ let dir = dirname5(fileURLToPath(import.meta.url));
5407
5634
  for (let i = 0; i < 8; i++) {
5408
- const candidate = join13(dir, "scripts", "detached-job.js");
5409
- if (existsSync14(candidate)) return candidate;
5410
- const parent = dirname4(dir);
5635
+ const candidate = join14(dir, "scripts", "detached-job.js");
5636
+ if (existsSync15(candidate)) return candidate;
5637
+ const parent = dirname5(dir);
5411
5638
  if (parent === dir) break;
5412
5639
  dir = parent;
5413
5640
  }
@@ -5635,14 +5862,15 @@ function formatLongRunningDirective(opts) {
5635
5862
  `Helper (same tool as \`scripts/detached-job.js\` when that file exists in the worktree): \`${invoke}\``,
5636
5863
  `- Start once: \`${invoke} start <id> -- <command> [args...]\` (cwd = this worktree). If JSON says already-running, do not start again.`,
5637
5864
  "- Immediately `present_artifact` `type=log` with `artifact_id=<id>` and `status=running` \u2014 the side column is the live view.",
5638
- `- Loop \`${invoke} wait <id>\` (returns in ~45s). stillRunning \u2192 present the same id with \`content=delta\` only \u2192 wait again. Do not resend the full log or HTML.`,
5865
+ `- Loop Sideboard MCP \`wait_for_job\` with the same id (returns in ~45s). stillRunning \u2192 present the same id with \`content=delta\` only \u2192 wait_for_job again. Shell fallback: \`${invoke} wait <id>\`. Do not resend the full log or HTML.`,
5866
+ "- Do not end the turn with \u201CI\u2019ll let you know when it\u2019s done.\u201D Stay in the loop until stillRunning is false.",
5639
5867
  "- ok \u2192 finish the task. failed \u2192 read the log, fix, start once.",
5640
5868
  "State: `.context/.sideboard/detached-jobs/<id>/` (local scratch). Full guide: `/long-running` (always available)."
5641
5869
  ].join("\n");
5642
5870
  }
5643
5871
  function formatLongRunningReminder(opts) {
5644
5872
  const invoke = formatDetachedJobInvoke(opts?.scriptPath);
5645
- return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait, present_artifact type=log (delta). Do not ask the human to poll.`;
5873
+ return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait_for_job (or detached-job wait), present_artifact type=log (delta). Do not say you will let the user know later \u2014 stay in the turn.`;
5646
5874
  }
5647
5875
  function formatArtifactDirective() {
5648
5876
  return [
@@ -5693,11 +5921,11 @@ function loadAgentInstructions(worktreePath, agent) {
5693
5921
  const out = [];
5694
5922
  for (const rel of candidates) {
5695
5923
  if (seenPaths.has(rel)) continue;
5696
- const abs = join14(worktreePath, rel);
5697
- if (!existsSync15(abs)) continue;
5924
+ const abs = join15(worktreePath, rel);
5925
+ if (!existsSync16(abs)) continue;
5698
5926
  try {
5699
5927
  if (!statSync4(abs).isFile()) continue;
5700
- let content = readFileSync11(abs, "utf8");
5928
+ let content = readFileSync12(abs, "utf8");
5701
5929
  if (!content.trim()) continue;
5702
5930
  if (content.length > MAX_CHARS_PER_FILE) {
5703
5931
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -6017,8 +6245,8 @@ async function syncThreadBranchFromGit(threadId) {
6017
6245
 
6018
6246
  // src/store/schedules.ts
6019
6247
  import { randomUUID as randomUUID4 } from "crypto";
6020
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
6021
- import { join as join15 } from "path";
6248
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
6249
+ import { join as join16 } from "path";
6022
6250
  import { Cron } from "croner";
6023
6251
  var DURATION_RE = /^(\d+)(s|m|h|d)$/i;
6024
6252
  var UNIT_MS = {
@@ -6028,7 +6256,7 @@ var UNIT_MS = {
6028
6256
  d: 864e5
6029
6257
  };
6030
6258
  function schedulesPath() {
6031
- return join15(appDataDir(), "schedules.json");
6259
+ return join16(appDataDir(), "schedules.json");
6032
6260
  }
6033
6261
  function parseDurationMs(every) {
6034
6262
  const m = every.trim().match(DURATION_RE);
@@ -6134,9 +6362,9 @@ function normalizeTask(raw) {
6134
6362
  }
6135
6363
  function readAll() {
6136
6364
  const path = schedulesPath();
6137
- if (!existsSync16(path)) return [];
6365
+ if (!existsSync17(path)) return [];
6138
6366
  try {
6139
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
6367
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
6140
6368
  if (!Array.isArray(parsed)) return [];
6141
6369
  return parsed.map((row) => {
6142
6370
  try {
@@ -6262,7 +6490,7 @@ function formatScheduledPrompt(name, prompt) {
6262
6490
  ${prompt}`;
6263
6491
  }
6264
6492
  async function defaultDeps() {
6265
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-BUH3LJLL.js");
6493
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-ZVW2AC6B.js");
6266
6494
  const orch = getOrchestrator2();
6267
6495
  return {
6268
6496
  findThread: (id) => findThreadByRef(id),
@@ -6419,6 +6647,9 @@ var Orchestrator = class {
6419
6647
  * finish or a user send so a later crash can recover again.
6420
6648
  */
6421
6649
  crashContinued = /* @__PURE__ */ new Set();
6650
+ /** Auto-continues after a worktree turn ended while a detached job still runs. */
6651
+ jobContinueCount = /* @__PURE__ */ new Map();
6652
+ jobContinueNudged = /* @__PURE__ */ new Set();
6422
6653
  maxConcurrent;
6423
6654
  runningCount = 0;
6424
6655
  constructor(opts) {
@@ -6502,7 +6733,7 @@ var Orchestrator = class {
6502
6733
  }
6503
6734
  continue;
6504
6735
  }
6505
- if (!existsSync17(thread.worktreePath)) {
6736
+ if (!existsSync18(thread.worktreePath)) {
6506
6737
  setStatus(thread.id, "broken", "Worktree missing on disk");
6507
6738
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
6508
6739
  continue;
@@ -6717,6 +6948,37 @@ var Orchestrator = class {
6717
6948
  this.emit({ type: "queue_changed", threadId, queue });
6718
6949
  this.haltDrain.delete(threadId);
6719
6950
  }
6951
+ /**
6952
+ * Worktree agent ended the turn after “I’ll let you know” (or left a
6953
+ * detached test/pack job running). Queue a continue so the chat does not
6954
+ * go idle with nobody watching the log.
6955
+ */
6956
+ maybeEnqueueJobContinue(threadId, chatText, parts = []) {
6957
+ if (this.haltDrain.has(threadId)) return;
6958
+ const thread = readThread(threadId);
6959
+ if (!thread || thread.status === "archived") return;
6960
+ const runningJobIds = listRunningDetachedJobs(thread.worktreePath ?? "");
6961
+ const decision = planJobContinue({
6962
+ runningJobIds,
6963
+ chatText,
6964
+ queueLength: thread.queue.length,
6965
+ continueCount: this.jobContinueCount.get(threadId) ?? 0,
6966
+ alreadyNudged: this.jobContinueNudged.has(threadId),
6967
+ isOrchestrator: isOrchestratorThread(thread),
6968
+ agent: thread.agent,
6969
+ watchedJob: turnWatchedDetachedJob(parts)
6970
+ });
6971
+ if (decision.action === "none") {
6972
+ if (runningJobIds.length === 0) this.jobContinueCount.delete(threadId);
6973
+ return;
6974
+ }
6975
+ if (decision.action === "nudge") this.jobContinueNudged.add(threadId);
6976
+ else this.jobContinueCount.set(threadId, (this.jobContinueCount.get(threadId) ?? 0) + 1);
6977
+ const queue = [decision.prompt, ...thread.queue];
6978
+ updateThread(threadId, { queue });
6979
+ this.emit({ type: "queue_changed", threadId, queue });
6980
+ this.haltDrain.delete(threadId);
6981
+ }
6720
6982
  getThreads(includeArchived = false) {
6721
6983
  return listThreads({ includeArchived });
6722
6984
  }
@@ -6816,6 +7078,8 @@ var Orchestrator = class {
6816
7078
  }
6817
7079
  const queue = [...current.queue, prompt];
6818
7080
  this.crashContinued.delete(thread.id);
7081
+ this.jobContinueCount.delete(thread.id);
7082
+ this.jobContinueNudged.delete(thread.id);
6819
7083
  this.haltDrain.delete(thread.id);
6820
7084
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id) || current.status === "running";
6821
7085
  shouldSteer = followUp === "steer" && (inFlight || current.queue.length > 0);
@@ -7330,6 +7594,7 @@ var Orchestrator = class {
7330
7594
  this.emit({ type: "turn_finished", threadId, exitCode });
7331
7595
  if (exitCode === 0) {
7332
7596
  this.crashContinued.delete(threadId);
7597
+ this.maybeEnqueueJobContinue(threadId, chatText, parts);
7333
7598
  } else {
7334
7599
  const blob = [chatText, detail].filter(Boolean).join("\n");
7335
7600
  void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
@@ -8330,7 +8595,7 @@ var Orchestrator = class {
8330
8595
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
8331
8596
  return restored2;
8332
8597
  }
8333
- if (!existsSync17(thread.worktreePath)) {
8598
+ if (!existsSync18(thread.worktreePath)) {
8334
8599
  if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
8335
8600
  throw new Error(
8336
8601
  `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
@@ -8592,6 +8857,11 @@ export {
8592
8857
  worktreeCleanupSettings,
8593
8858
  applyThreadIntoMain,
8594
8859
  cloneRepoIntoSideboard,
8860
+ mcpWaitForTurnTimeoutMs,
8861
+ mcpWaitStillRunningHint,
8862
+ mcpWaitFinishedHint,
8863
+ mcpWaitForJobTimeoutMs,
8864
+ waitForDetachedJob,
8595
8865
  detectAgents,
8596
8866
  requireAgent,
8597
8867
  isHomeBoardThread,