@sideboard-ai/core 0.1.241 → 0.1.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{abletime-VSV6AUBV.js → abletime-CWIJ7INC.js} +4 -4
  2. package/dist/{abletime-UX4VUENQ.js → abletime-OI6FNSXX.js} +4 -4
  3. package/dist/{abletime-oauth-DKBUZV2C.js → abletime-oauth-CHVTUZBR.js} +3 -3
  4. package/dist/{abletime-oauth-QMDV45JC.js → abletime-oauth-PUIPZTA4.js} +3 -3
  5. package/dist/{abletime-rest-NL56UVK5.js → abletime-rest-32NIVXAY.js} +3 -3
  6. package/dist/{abletime-rest-VPBC2XS5.js → abletime-rest-APJUKFKL.js} +3 -3
  7. package/dist/{agents-CDQCQVWU.js → agents-CPME6O2H.js} +9 -5
  8. package/dist/{agents-PBRIHUYM.js → agents-VKO5UZO6.js} +9 -5
  9. package/dist/{app-settings-AGDLF75Z.js → app-settings-652J57CX.js} +1 -1
  10. package/dist/{app-settings-I27UBVI4.js → app-settings-VOPXJMGV.js} +1 -1
  11. package/dist/{chunk-BILEJG3V.js → chunk-3P5OCI2X.js} +3 -3
  12. package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
  13. package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
  14. package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
  15. package/dist/{chunk-TB6UMHIF.js → chunk-7UCZ4DRF.js} +2 -2
  16. package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
  17. package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
  18. package/dist/{chunk-O3UAQAYA.js → chunk-CCD2QIFK.js} +2 -2
  19. package/dist/{chunk-63UHRQ64.js → chunk-DARSRQUE.js} +1523 -1450
  20. package/dist/{chunk-OKHOYI53.js → chunk-ECZTXNYR.js} +3 -3
  21. package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
  22. package/dist/{chunk-3Q4PVZ3U.js → chunk-HR4RNC73.js} +32 -5
  23. package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
  24. package/dist/{chunk-QXYWLO6W.js → chunk-ISZERVDT.js} +28 -5
  25. package/dist/{chunk-EE3REM4B.js → chunk-NEOAHPMK.js} +1582 -1505
  26. package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
  27. package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
  28. package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
  29. package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
  30. package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
  31. package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
  32. package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-AJ3E6LBX.js} +4 -4
  33. package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-PYIRQGU7.js} +4 -4
  34. package/dist/{global-workspace-TD5SARNW.js → global-workspace-GIQ6F6IJ.js} +4 -4
  35. package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-GQM653DP.js} +4 -4
  36. package/dist/index.cjs +609 -463
  37. package/dist/index.d.cts +56 -2
  38. package/dist/index.d.ts +56 -2
  39. package/dist/index.js +74 -38
  40. package/dist/mcp/run-stdio.cjs +1029 -908
  41. package/dist/mcp/run-stdio.js +50 -36
  42. package/dist/{orchestrator-ZUOEBJFN.js → orchestrator-3IOISDFS.js} +7 -7
  43. package/dist/{orchestrator-7AJEFWGQ.js → orchestrator-YDPDMXUY.js} +7 -7
  44. package/dist/{plan-file-UUKCZEYP.js → plan-file-7MMIPV7Y.js} +9 -1
  45. package/dist/{plan-file-2EPHYRX7.js → plan-file-CKXUFTQ2.js} +9 -1
  46. package/dist/{thread-store-YOPKY74K.js → thread-store-JQB7KZJR.js} +1 -1
  47. package/dist/{thread-store-AYPBEZT5.js → thread-store-SCV2IZYS.js} +1 -1
  48. package/dist/{workspaces-4SFSCYKT.js → workspaces-OIBSD5B6.js} +4 -4
  49. package/dist/{workspaces-JX5R4V24.js → workspaces-WYC5TLC3.js} +4 -4
  50. package/dist/{worktree-Y5FES5D6.js → worktree-5VRZVGP3.js} +3 -3
  51. package/dist/{worktree-AGVPKDBH.js → worktree-DGDM7LGV.js} +3 -3
  52. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1240,6 +1240,7 @@ function archivedUpdatedMs(thread) {
1240
1240
  }
1241
1241
  function isHistoryRetentionStub(thread) {
1242
1242
  if ((thread.attachments?.length ?? 0) > 0) return false;
1243
+ if ((thread.compactedMessages?.length ?? 0) > 0) return false;
1243
1244
  const messages = thread.messages ?? [];
1244
1245
  if (messages.length === 0) return true;
1245
1246
  if (messages.length > 1) return false;
@@ -3323,7 +3324,8 @@ function normalizeThread(raw) {
3323
3324
  runScriptRequest: normalizeRunScriptRequest(raw.runScriptRequest),
3324
3325
  quotaResumeAt: raw.quotaResumeAt ?? null,
3325
3326
  quotaContinuedFromId: raw.quotaContinuedFromId ?? null,
3326
- archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0
3327
+ archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0,
3328
+ compactedMessages: Array.isArray(raw.compactedMessages) ? raw.compactedMessages : void 0
3327
3329
  };
3328
3330
  }
3329
3331
  function createEmptyThread(partial) {
@@ -4415,6 +4417,16 @@ function skillToolFromTextPart(part, id) {
4415
4417
  updatedAt: Date.now()
4416
4418
  };
4417
4419
  }
4420
+ function compactSummaryPart(text7, id = "compact-summary") {
4421
+ return compactToolPart(text7, id, { status: "done" });
4422
+ }
4423
+ function compactPartsForSummaryMessage(message) {
4424
+ const existing = (message.parts ?? []).filter(
4425
+ (p) => p.type === "tool" && isCompactToolName(p.name)
4426
+ );
4427
+ if (existing.length) return existing;
4428
+ return [compactSummaryPart(message.text ?? "")];
4429
+ }
4418
4430
  function compactToolPart(text7, id, opts) {
4419
4431
  const input = { trigger: opts?.trigger ?? "auto" };
4420
4432
  return withPartTimes(
@@ -12360,6 +12372,10 @@ var init_node_launch = __esm({
12360
12372
  // src/agents/injected-mcp.ts
12361
12373
  function sideboardWorktreeAllowedTools(opts) {
12362
12374
  const out = [...SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS];
12375
+ if (!opts?.planMode) {
12376
+ const idx = out.indexOf("mcp__sideboard__present_plan");
12377
+ if (idx >= 0) out.splice(idx, 1);
12378
+ }
12363
12379
  if (opts?.github !== false) out.push(...SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS);
12364
12380
  if (opts?.linear) out.push(...SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS);
12365
12381
  if (opts?.abletime) out.push(...SIDEBOARD_ABLETIME_MCP_ALLOWED_TOOLS);
@@ -12906,6 +12922,9 @@ var init_orch_mcp_isolation = __esm({
12906
12922
  });
12907
12923
 
12908
12924
  // src/agents/types.ts
12925
+ function planModeTurnInstruction(planMode) {
12926
+ return planMode ? PLAN_MODE_INSTRUCTION : PLAN_MODE_OFF_INSTRUCTION;
12927
+ }
12909
12928
  function permissionMode(thread) {
12910
12929
  if (thread.sourceType === "orchestration") {
12911
12930
  return {
@@ -12944,11 +12963,12 @@ function permissionMode(thread) {
12944
12963
  codexSandbox: "workspace-write"
12945
12964
  };
12946
12965
  }
12947
- var PLAN_MODE_INSTRUCTION;
12966
+ var PLAN_MODE_INSTRUCTION, PLAN_MODE_OFF_INSTRUCTION;
12948
12967
  var init_types = __esm({
12949
12968
  "src/agents/types.ts"() {
12950
12969
  "use strict";
12951
12970
  PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or Approves / Hands off the plan). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any project files except via Sideboard MCP present_plan (writes .context/attachments/plan.md). When you need a clarifying decision (approach forks, auth choice, scope) \u2014 not greetings, check-ins, or an invented task menu: (1) first write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it) \u2014 do not leave the user staring at bare labels; (2) then call Sideboard MCP ask_user with the same options, including a description on every option. If one option is the obvious default, proceed without asking. Sideboard shows questions in the composer and mirrors them in chat. After ask_user, wait for the user's next message with their answers before finalizing the plan. When the plan is ready for approval: (1) call present_plan with the full markdown plan (title + content) so Sideboard saves .context/attachments/plan.md and shows it in chat for Approve / Hand off / Copy; (2) Claude should also call ExitPlanMode after present_plan. Do not skip present_plan \u2014 the plan must be a markdown file, not only chat prose.";
12971
+ PLAN_MODE_OFF_INSTRUCTION = "Plan mode is off. Do not call present_plan or ExitPlanMode \u2014 those only work when the user turns Plan on in the composer. If you need to propose an approach, write it in chat.";
12952
12972
  }
12953
12973
  });
12954
12974
 
@@ -13461,7 +13481,8 @@ var init_claude = __esm({
13461
13481
  ...sideboardWorktreeAllowedTools({
13462
13482
  github: true,
13463
13483
  linear: isLinearConnected(),
13464
- abletime: isAbleTimeConnected()
13484
+ abletime: isAbleTimeConnected(),
13485
+ planMode: thread.planMode
13465
13486
  }),
13466
13487
  ...brightsyMcpAllowedTools(injectedBrightsyNames)
13467
13488
  ];
@@ -15879,6 +15900,7 @@ __export(agents_exports, {
15879
15900
  CURSOR_SDK_INSTALL_HINT: () => CURSOR_SDK_INSTALL_HINT,
15880
15901
  ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
15881
15902
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
15903
+ PLAN_MODE_OFF_INSTRUCTION: () => PLAN_MODE_OFF_INSTRUCTION,
15882
15904
  allAdapters: () => allAdapters,
15883
15905
  assertOrchestratorCapableAgent: () => assertOrchestratorCapableAgent,
15884
15906
  brightsyAdapter: () => brightsyAdapter,
@@ -15929,6 +15951,7 @@ __export(agents_exports, {
15929
15951
  parseCursorRunnerLine: () => parseCursorRunnerLine,
15930
15952
  parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
15931
15953
  permissionMode: () => permissionMode,
15954
+ planModeTurnInstruction: () => planModeTurnInstruction,
15932
15955
  posixShellSingleQuote: () => posixShellSingleQuote,
15933
15956
  preferredCursorCostCents: () => preferredCursorCostCents,
15934
15957
  prepareTerminalCommand: () => prepareTerminalCommand,
@@ -16911,12 +16934,38 @@ ${match.text}`, "");
16911
16934
  blocks.push("Continue from this context. Do not repeat the summary unless asked.");
16912
16935
  return blocks.join("\n");
16913
16936
  }
16937
+ function compactArchiveProse(message) {
16938
+ if (message.origin === "continue") return null;
16939
+ if (message.role === "summary") return null;
16940
+ let text7 = (message.text ?? "").trim();
16941
+ if (!text7 && message.role === "agent") {
16942
+ text7 = lastAssistantMessageText(message.parts).trim();
16943
+ }
16944
+ if (!text7 || isInternalAgentStatusText(text7)) return null;
16945
+ return { role: message.role, text: text7, ts: message.ts };
16946
+ }
16947
+ function messagesForCompactArchive(older) {
16948
+ const out = [];
16949
+ for (const message of older) {
16950
+ const prose = compactArchiveProse(message);
16951
+ if (prose) out.push(prose);
16952
+ }
16953
+ return out;
16954
+ }
16955
+ function appendCompactedMessages(existing, older) {
16956
+ const next = messagesForCompactArchive(older);
16957
+ if (!existing?.length) return next;
16958
+ if (!next.length) return existing;
16959
+ return [...existing, ...next];
16960
+ }
16914
16961
  function applyCompaction(messages, summaryText, thresholds = {}) {
16915
16962
  const { older, recent } = splitForCompaction(messages, thresholds);
16916
16963
  if (older.length === 0) return messages;
16964
+ const text7 = summaryText.trim();
16917
16965
  const summary = {
16918
16966
  role: "summary",
16919
- text: summaryText.trim(),
16967
+ text: text7,
16968
+ parts: [compactSummaryPart(text7)],
16920
16969
  ts: (/* @__PURE__ */ new Date()).toISOString()
16921
16970
  };
16922
16971
  return [summary, ...recent];
@@ -16944,6 +16993,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
16944
16993
  cwd: thread.worktreePath
16945
16994
  });
16946
16995
  let messages = applyCompaction(thread.messages, summary, thresholds);
16996
+ const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
16947
16997
  const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
16948
16998
  if (resetSession) {
16949
16999
  messages = applyForwardOccupancy(messages);
@@ -16951,6 +17001,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
16951
17001
  const next = {
16952
17002
  ...thread,
16953
17003
  messages,
17004
+ ...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
16954
17005
  sessionId: resetSession ? null : thread.sessionId,
16955
17006
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16956
17007
  };
@@ -16966,6 +17017,7 @@ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAG
16966
17017
  var init_context_compact = __esm({
16967
17018
  "src/composer/context-compact.ts"() {
16968
17019
  "use strict";
17020
+ init_message_parts();
16969
17021
  init_error_detail();
16970
17022
  init_usage();
16971
17023
  init_context_estimate();
@@ -17552,15 +17604,403 @@ var init_worktree_run_ports = __esm({
17552
17604
  }
17553
17605
  });
17554
17606
 
17607
+ // src/mcp/wait-for-turn.ts
17608
+ function mcpWaitForTurnTimeoutMs(requested) {
17609
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
17610
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
17611
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
17612
+ }
17613
+ function mcpWaitTaskHint(taskState, status) {
17614
+ switch (taskState) {
17615
+ case "submitted":
17616
+ return MCP_WAIT_QUEUED_HINT;
17617
+ case "working":
17618
+ return MCP_WAIT_STILL_RUNNING_HINT;
17619
+ case "input-required":
17620
+ return MCP_WAIT_INPUT_REQUIRED_HINT;
17621
+ case "canceled":
17622
+ return MCP_WAIT_STOPPED_HINT;
17623
+ case "failed":
17624
+ return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
17625
+ default:
17626
+ return void 0;
17627
+ }
17628
+ }
17629
+ var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT, MCP_WAIT_INPUT_REQUIRED_HINT;
17630
+ var init_wait_for_turn = __esm({
17631
+ "src/mcp/wait-for-turn.ts"() {
17632
+ "use strict";
17633
+ MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
17634
+ MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
17635
+ MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
17636
+ 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.";
17637
+ MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
17638
+ 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.";
17639
+ MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
17640
+ }
17641
+ });
17642
+
17643
+ // src/mcp/wait-for-job.ts
17644
+ function mcpWaitForJobTimeoutMs(requested) {
17645
+ return mcpWaitForTurnTimeoutMs(requested);
17646
+ }
17647
+ function sanitizeDetachedJobId(id) {
17648
+ const s = id.trim();
17649
+ if (!JOB_ID_RE.test(s)) {
17650
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
17651
+ }
17652
+ return s;
17653
+ }
17654
+ function jobAlive(pid) {
17655
+ if (!Number.isInteger(pid) || pid <= 0) return false;
17656
+ try {
17657
+ process.kill(pid, 0);
17658
+ return true;
17659
+ } catch {
17660
+ return false;
17661
+ }
17662
+ }
17663
+ function processGroupAlive(pgid) {
17664
+ if (process.platform === "win32") return false;
17665
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
17666
+ try {
17667
+ process.kill(-pgid, 0);
17668
+ return true;
17669
+ } catch (err) {
17670
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
17671
+ }
17672
+ }
17673
+ function jobTreeAlive(pid) {
17674
+ if (pid == null) return false;
17675
+ return jobAlive(pid) || processGroupAlive(pid);
17676
+ }
17677
+ function readIntFile(file) {
17678
+ if (!(0, import_node_fs45.existsSync)(file)) return null;
17679
+ const n = Number.parseInt((0, import_node_fs45.readFileSync)(file, "utf8").trim(), 10);
17680
+ return Number.isInteger(n) ? n : null;
17681
+ }
17682
+ function jobDir(root, id, legacy = false) {
17683
+ return (0, import_node_path45.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
17684
+ }
17685
+ function resolveJobDir(root, id) {
17686
+ const modern = jobDir(root, id, false);
17687
+ if ((0, import_node_fs45.existsSync)(modern)) return modern;
17688
+ const legacy = jobDir(root, id, true);
17689
+ if ((0, import_node_fs45.existsSync)(legacy)) return legacy;
17690
+ return modern;
17691
+ }
17692
+ function listJobIdsIn(root, rel) {
17693
+ const dir = (0, import_node_path45.join)(root, rel);
17694
+ if (!(0, import_node_fs45.existsSync)(dir)) return [];
17695
+ return (0, import_node_fs45.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
17696
+ }
17697
+ function listRunningDetachedJobs(worktreePath) {
17698
+ const root = worktreePath.trim();
17699
+ if (!root) return [];
17700
+ const ids = /* @__PURE__ */ new Set([
17701
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
17702
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
17703
+ ]);
17704
+ const running = [];
17705
+ for (const id of ids) {
17706
+ const dir = resolveJobDir(root, id);
17707
+ const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
17708
+ if (jobTreeAlive(pid)) running.push(id);
17709
+ }
17710
+ return running.sort();
17711
+ }
17712
+ function looksLikeDeferredDonePromise(text7) {
17713
+ const t = (text7 ?? "").trim();
17714
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
17715
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
17716
+ if (!unquoted) return false;
17717
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
17718
+ }
17719
+ function isJobContinuePrompt(text7) {
17720
+ if (typeof text7 !== "string") return false;
17721
+ const t = text7.trim();
17722
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
17723
+ }
17724
+ function collapseGhRunWatchLog(text7) {
17725
+ if (!text7.includes(GH_WATCH_REFRESH)) return text7;
17726
+ const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
17727
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
17728
+ if (watchFrames.length <= 1) return text7;
17729
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
17730
+ const last = watchFrames[watchFrames.length - 1] ?? "";
17731
+ const omitted = watchFrames.length - 1;
17732
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
17733
+ `;
17734
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
17735
+ }
17736
+ function capJobLogDelta(text7) {
17737
+ const collapsed = collapseGhRunWatchLog(text7);
17738
+ const lines = collapsed.split("\n");
17739
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
17740
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
17741
+ if (out.length > MAX_JOB_DELTA_CHARS) {
17742
+ out = `\u2026(truncated ${out.length} chars)
17743
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
17744
+ }
17745
+ return out;
17746
+ }
17747
+ function formatJobStillRunningContinuePrompt(jobIds) {
17748
+ const ids = jobIds.join(", ");
17749
+ return [
17750
+ `Detached job still running: ${ids}.`,
17751
+ "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
17752
+ "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
17753
+ "Then report the result. Do not tell the user you will let them know later."
17754
+ ].join(" ");
17755
+ }
17756
+ function formatDeferredDoneContinuePrompt() {
17757
+ return [
17758
+ "You ended the turn after promising to report later, but no detached job is running.",
17759
+ "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
17760
+ "Do not say you will let the user know later."
17761
+ ].join(" ");
17762
+ }
17763
+ function turnWatchedDetachedJob(parts) {
17764
+ return parts.some((p) => {
17765
+ if (p.type !== "tool") return false;
17766
+ if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
17767
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
17768
+ return /detached-job\.(?:js|cjs)\b/i.test(blob);
17769
+ });
17770
+ }
17771
+ function planJobContinue(opts) {
17772
+ if (opts.isOrchestrator) return { action: "none" };
17773
+ if (opts.agent === "brightsy") return { action: "none" };
17774
+ if (opts.queueLength > 0) return { action: "none" };
17775
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
17776
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
17777
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
17778
+ return {
17779
+ action: "wait",
17780
+ jobIds: opts.runningJobIds,
17781
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
17782
+ };
17783
+ }
17784
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
17785
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
17786
+ }
17787
+ return { action: "none" };
17788
+ }
17789
+ function tailProgress(logFile, maxLines = 12) {
17790
+ if (!(0, import_node_fs45.existsSync)(logFile)) return "(no log yet)";
17791
+ const lines = (0, import_node_fs45.readFileSync)(logFile, "utf8").split("\n");
17792
+ return lines.slice(-maxLines).join("\n");
17793
+ }
17794
+ function lastProgressLine(progress) {
17795
+ const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
17796
+ return last ? last.slice(0, 72) : void 0;
17797
+ }
17798
+ function readLogLines(file) {
17799
+ if (!(0, import_node_fs45.existsSync)(file)) return [];
17800
+ const lines = (0, import_node_fs45.readFileSync)(file, "utf8").split("\n");
17801
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
17802
+ return lines;
17803
+ }
17804
+ function takeDelta(logFile, cursorFile) {
17805
+ const lines = readLogLines(logFile);
17806
+ const cursor = readIntFile(cursorFile) ?? 0;
17807
+ const start = Math.min(Math.max(0, cursor), lines.length);
17808
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
17809
+ }
17810
+ function snapshotJob(dir) {
17811
+ const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
17812
+ const running = jobTreeAlive(pid);
17813
+ return {
17814
+ pid,
17815
+ running,
17816
+ exitCode: readIntFile((0, import_node_path45.join)(dir, "exit")),
17817
+ log: (0, import_node_path45.join)(dir, "log"),
17818
+ cursor: (0, import_node_path45.join)(dir, "present.cursor"),
17819
+ progress: tailProgress((0, import_node_path45.join)(dir, "log"))
17820
+ };
17821
+ }
17822
+ function toResult(id, snap, extra) {
17823
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
17824
+ const ok = !snap.running && snap.exitCode === 0;
17825
+ const stillRunning = snap.running && !ok;
17826
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
17827
+ try {
17828
+ (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(snap.cursor), { recursive: true });
17829
+ (0, import_node_fs45.writeFileSync)(snap.cursor, `${nextCursor}
17830
+ `);
17831
+ } catch {
17832
+ }
17833
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
17834
+ const progress = extra?.progress ?? snap.progress;
17835
+ return {
17836
+ stillRunning,
17837
+ ok,
17838
+ failed: Boolean(failed && !stillRunning && !ok),
17839
+ status,
17840
+ id,
17841
+ pid: snap.pid,
17842
+ exitCode: snap.exitCode ?? void 0,
17843
+ phase: lastProgressLine(progress),
17844
+ delta,
17845
+ progress,
17846
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
17847
+ };
17848
+ }
17849
+ async function sleepMs(ms) {
17850
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
17851
+ }
17852
+ async function waitForDetachedJob(cwd, id, opts) {
17853
+ const jobId = sanitizeDetachedJobId(id);
17854
+ const root = cwd.trim() || process.cwd();
17855
+ const dir = resolveJobDir(root, jobId);
17856
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
17857
+ const sleep2 = opts?.sleep ?? sleepMs;
17858
+ if (!(0, import_node_fs45.existsSync)(dir)) {
17859
+ return {
17860
+ stillRunning: false,
17861
+ ok: false,
17862
+ failed: true,
17863
+ status: "failed",
17864
+ id: jobId,
17865
+ delta: "",
17866
+ progress: "No detached job. Start one first.",
17867
+ hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
17868
+ };
17869
+ }
17870
+ const deadline = Date.now() + timeoutMs;
17871
+ let snap = snapshotJob(dir);
17872
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
17873
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
17874
+ return toResult(jobId, snap, {
17875
+ failed: true,
17876
+ progress: "No detached job. Start one first."
17877
+ });
17878
+ }
17879
+ while (Date.now() < deadline) {
17880
+ snap = snapshotJob(dir);
17881
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
17882
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
17883
+ await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
17884
+ }
17885
+ snap = snapshotJob(dir);
17886
+ return toResult(jobId, snap);
17887
+ }
17888
+ function killProcessTree(pid, signal) {
17889
+ if (!Number.isInteger(pid) || pid <= 0) return;
17890
+ if (process.platform !== "win32") {
17891
+ try {
17892
+ process.kill(-pid, signal);
17893
+ return;
17894
+ } catch {
17895
+ }
17896
+ }
17897
+ try {
17898
+ process.kill(pid, signal);
17899
+ } catch {
17900
+ }
17901
+ }
17902
+ function appendJobLog(logFile, line) {
17903
+ try {
17904
+ (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(logFile), { recursive: true });
17905
+ const body = line.endsWith("\n") ? line : `${line}
17906
+ `;
17907
+ (0, import_node_fs45.appendFileSync)(logFile, body);
17908
+ } catch {
17909
+ }
17910
+ }
17911
+ async function stopDetachedJob(cwd, id, opts) {
17912
+ const jobId = sanitizeDetachedJobId(id);
17913
+ const root = cwd.trim() || process.cwd();
17914
+ const dir = resolveJobDir(root, jobId);
17915
+ const sleep2 = opts?.sleep ?? sleepMs;
17916
+ const why = opts?.reason?.trim() ?? "";
17917
+ if (!(0, import_node_fs45.existsSync)(dir)) {
17918
+ return {
17919
+ stillRunning: false,
17920
+ ok: false,
17921
+ failed: true,
17922
+ status: "failed",
17923
+ id: jobId,
17924
+ delta: "",
17925
+ progress: "No detached job. Start one first.",
17926
+ hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
17927
+ stopped: false,
17928
+ reason: "not-found"
17929
+ };
17930
+ }
17931
+ let snap = snapshotJob(dir);
17932
+ if (!snap.running) {
17933
+ const result2 = toResult(jobId, snap);
17934
+ return {
17935
+ ...result2,
17936
+ stopped: false,
17937
+ reason: "not-running",
17938
+ hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
17939
+ };
17940
+ }
17941
+ appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
17942
+ const targetPid = snap.pid;
17943
+ if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
17944
+ const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
17945
+ const deadline = Date.now() + graceMs;
17946
+ while (Date.now() < deadline && jobTreeAlive(targetPid)) {
17947
+ await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
17948
+ }
17949
+ if (targetPid != null) {
17950
+ killProcessTree(targetPid, "SIGKILL");
17951
+ await sleep2(100);
17952
+ }
17953
+ const exitFile = (0, import_node_path45.join)(dir, "exit");
17954
+ if (!(0, import_node_fs45.existsSync)(exitFile)) {
17955
+ try {
17956
+ (0, import_node_fs45.writeFileSync)(exitFile, "1\n");
17957
+ } catch {
17958
+ }
17959
+ }
17960
+ snap = snapshotJob(dir);
17961
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
17962
+ const result = toResult(jobId, snap, { failed: true });
17963
+ const stopped = !stillRunning;
17964
+ return {
17965
+ ...result,
17966
+ stillRunning,
17967
+ ok: false,
17968
+ failed: true,
17969
+ status: stillRunning ? "running" : "failed",
17970
+ stopped,
17971
+ reason: stopped ? "stopped" : "still-running",
17972
+ stopReason: why || void 0,
17973
+ hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
17974
+ };
17975
+ }
17976
+ var import_node_fs45, import_node_path45, MAX_JOB_CONTINUES, MAX_JOB_DELTA_LINES, MAX_JOB_DELTA_CHARS, DEFERRED_DONE_MAX_CHARS, GH_WATCH_REFRESH, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
17977
+ var init_wait_for_job = __esm({
17978
+ "src/mcp/wait-for-job.ts"() {
17979
+ "use strict";
17980
+ import_node_fs45 = require("fs");
17981
+ import_node_path45 = require("path");
17982
+ init_workspace_scratch();
17983
+ init_wait_for_turn();
17984
+ MAX_JOB_CONTINUES = 8;
17985
+ MAX_JOB_DELTA_LINES = 80;
17986
+ MAX_JOB_DELTA_CHARS = 4e3;
17987
+ DEFERRED_DONE_MAX_CHARS = 480;
17988
+ GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
17989
+ MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
17990
+ MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
17991
+ JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
17992
+ }
17993
+ });
17994
+
17555
17995
  // src/hook/conductor.ts
17556
17996
  function matchSimpleGlob(pattern, name) {
17557
17997
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
17558
17998
  return new RegExp(`^${escaped}$`).test(name);
17559
17999
  }
17560
18000
  function readWorktreeInclude(repoPath) {
17561
- const path2 = (0, import_node_path45.join)(repoPath, ".worktreeinclude");
17562
- if (!(0, import_node_fs45.existsSync)(path2)) return [];
17563
- return (0, import_node_fs45.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
18001
+ const path2 = (0, import_node_path46.join)(repoPath, ".worktreeinclude");
18002
+ if (!(0, import_node_fs46.existsSync)(path2)) return [];
18003
+ return (0, import_node_fs46.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
17564
18004
  }
17565
18005
  function resolveFilesToCopy(repoPath) {
17566
18006
  const fromInclude = readWorktreeInclude(repoPath);
@@ -17570,10 +18010,10 @@ function resolveFilesToCopy(repoPath) {
17570
18010
  if (settings?.fileIncludeGlobs?.length) {
17571
18011
  const matched = [];
17572
18012
  try {
17573
- for (const entry of (0, import_node_fs45.readdirSync)(repoPath, { withFileTypes: true })) {
18013
+ for (const entry of (0, import_node_fs46.readdirSync)(repoPath, { withFileTypes: true })) {
17574
18014
  if (!entry.isFile()) continue;
17575
18015
  for (const glob of settings.fileIncludeGlobs) {
17576
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path45.basename)(glob), entry.name)) {
18016
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path46.basename)(glob), entry.name)) {
17577
18017
  matched.push(entry.name);
17578
18018
  break;
17579
18019
  }
@@ -17585,7 +18025,7 @@ function resolveFilesToCopy(repoPath) {
17585
18025
  }
17586
18026
  const defaults = [];
17587
18027
  try {
17588
- for (const entry of (0, import_node_fs45.readdirSync)(repoPath, { withFileTypes: true })) {
18028
+ for (const entry of (0, import_node_fs46.readdirSync)(repoPath, { withFileTypes: true })) {
17589
18029
  if (entry.isFile() && entry.name.startsWith(".env")) {
17590
18030
  defaults.push(entry.name);
17591
18031
  }
@@ -17599,11 +18039,11 @@ function copyConfiguredFiles(repoPath, worktreePath) {
17599
18039
  const patterns = resolveFilesToCopy(repoPath);
17600
18040
  const copied = [];
17601
18041
  for (const rel of patterns) {
17602
- const src = (0, import_node_path45.join)(repoPath, rel);
17603
- if (!(0, import_node_fs45.existsSync)(src)) continue;
17604
- const dest = (0, import_node_path45.join)(worktreePath, rel);
17605
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(dest), { recursive: true });
17606
- (0, import_node_fs45.copyFileSync)(src, dest);
18042
+ const src = (0, import_node_path46.join)(repoPath, rel);
18043
+ if (!(0, import_node_fs46.existsSync)(src)) continue;
18044
+ const dest = (0, import_node_path46.join)(worktreePath, rel);
18045
+ (0, import_node_fs46.mkdirSync)((0, import_node_path46.dirname)(dest), { recursive: true });
18046
+ (0, import_node_fs46.copyFileSync)(src, dest);
17607
18047
  copied.push(rel);
17608
18048
  }
17609
18049
  return copied;
@@ -17638,7 +18078,7 @@ function buildWorkspaceScriptEnv(opts, baseEnv) {
17638
18078
  const env = stripNestedElectronEnv({
17639
18079
  ...baseEnv ?? process.env
17640
18080
  });
17641
- const name = opts.workspaceName ?? (0, import_node_path45.basename)(opts.worktreePath);
18081
+ const name = opts.workspaceName ?? (0, import_node_path46.basename)(opts.worktreePath);
17642
18082
  const ports = opts.ports ?? [];
17643
18083
  const primary = ports[0];
17644
18084
  env.SIDEBOARD_WORKSPACE_NAME = name;
@@ -17707,20 +18147,20 @@ function listenerCwd(pid) {
17707
18147
  }
17708
18148
  function realpathOrResolve(path2) {
17709
18149
  try {
17710
- return (0, import_node_fs45.realpathSync)(path2);
18150
+ return (0, import_node_fs46.realpathSync)(path2);
17711
18151
  } catch {
17712
- return (0, import_node_path45.resolve)(path2);
18152
+ return (0, import_node_path46.resolve)(path2);
17713
18153
  }
17714
18154
  }
17715
18155
  function isOtherWorktreeListenerCwd(cwd, worktreePath) {
17716
18156
  const wt = realpathOrResolve(worktreePath);
17717
18157
  const c = realpathOrResolve(cwd);
17718
- if (c === wt || c.startsWith(wt + import_node_path45.sep)) return false;
17719
- const parent = (0, import_node_path45.dirname)(wt);
17720
- const thisName = (0, import_node_path45.basename)(wt);
18158
+ if (c === wt || c.startsWith(wt + import_node_path46.sep)) return false;
18159
+ const parent = (0, import_node_path46.dirname)(wt);
18160
+ const thisName = (0, import_node_path46.basename)(wt);
17721
18161
  if (!thisName || c === parent) return false;
17722
- if (!c.startsWith(parent + import_node_path45.sep)) return false;
17723
- const first = c.slice(parent.length + 1).split(import_node_path45.sep)[0];
18162
+ if (!c.startsWith(parent + import_node_path46.sep)) return false;
18163
+ const first = c.slice(parent.length + 1).split(import_node_path46.sep)[0];
17724
18164
  return Boolean(first) && first !== thisName;
17725
18165
  }
17726
18166
  function signalPid(pid, signal) {
@@ -17729,6 +18169,25 @@ function signalPid(pid, signal) {
17729
18169
  } catch {
17730
18170
  }
17731
18171
  }
18172
+ function isThisWorktreeListenerCwd(cwd, worktreePath) {
18173
+ const wt = realpathOrResolve(worktreePath);
18174
+ const c = realpathOrResolve(cwd);
18175
+ return c === wt || c.startsWith(wt + import_node_path46.sep);
18176
+ }
18177
+ function killDetachedRunTree(pid, worktreePath) {
18178
+ if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
18179
+ const cwd = listenerCwd(pid);
18180
+ if (cwd) {
18181
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
18182
+ } else if (!processGroupAlive(pid)) {
18183
+ return;
18184
+ }
18185
+ try {
18186
+ process.kill(-pid, "SIGTERM");
18187
+ } catch {
18188
+ signalPid(pid, "SIGTERM");
18189
+ }
18190
+ }
17732
18191
  function killListenersOnPorts(ports) {
17733
18192
  const selfPid = process.pid;
17734
18193
  for (const port of ports) {
@@ -18119,14 +18578,14 @@ async function startDevServer(repoPath, worktreePath, onLine, opts) {
18119
18578
  throw err;
18120
18579
  }
18121
18580
  }
18122
- var import_node_fs45, import_node_child_process5, import_node_net, import_node_path45, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
18581
+ var import_node_fs46, import_node_child_process5, import_node_net, import_node_path46, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
18123
18582
  var init_conductor = __esm({
18124
18583
  "src/hook/conductor.ts"() {
18125
18584
  "use strict";
18126
- import_node_fs45 = require("fs");
18585
+ import_node_fs46 = require("fs");
18127
18586
  import_node_child_process5 = require("child_process");
18128
18587
  import_node_net = require("net");
18129
- import_node_path45 = require("path");
18588
+ import_node_path46 = require("path");
18130
18589
  import_execa4 = require("execa");
18131
18590
  import_node_readline3 = require("readline");
18132
18591
  init_settings();
@@ -18138,6 +18597,7 @@ var init_conductor = __esm({
18138
18597
  init_request_review();
18139
18598
  init_worktree_exclude();
18140
18599
  init_worktree_run_ports();
18600
+ init_wait_for_job();
18141
18601
  init_worktree_run_ports();
18142
18602
  init_nested_electron_env();
18143
18603
  PORT_RANGE_SIZE = 10;
@@ -18158,9 +18618,9 @@ async function findOrphanWorktrees(repoPaths) {
18158
18618
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
18159
18619
  );
18160
18620
  const homeRoot = sideboardWorkspacesDir();
18161
- if ((0, import_node_fs46.existsSync)(homeRoot)) {
18621
+ if ((0, import_node_fs47.existsSync)(homeRoot)) {
18162
18622
  try {
18163
- for (const entry of (0, import_node_fs46.readdirSync)(homeRoot, { withFileTypes: true })) {
18623
+ for (const entry of (0, import_node_fs47.readdirSync)(homeRoot, { withFileTypes: true })) {
18164
18624
  if (!entry.isDirectory()) continue;
18165
18625
  void entry;
18166
18626
  }
@@ -18170,7 +18630,7 @@ async function findOrphanWorktrees(repoPaths) {
18170
18630
  const orphans = [];
18171
18631
  const seen = /* @__PURE__ */ new Set();
18172
18632
  for (const repoPath of repos) {
18173
- if (!repoPath || !(0, import_node_fs46.existsSync)(repoPath)) continue;
18633
+ if (!repoPath || !(0, import_node_fs47.existsSync)(repoPath)) continue;
18174
18634
  try {
18175
18635
  const wts = await listWorktrees(repoPath);
18176
18636
  for (const wt of wts) {
@@ -18181,7 +18641,7 @@ async function findOrphanWorktrees(repoPaths) {
18181
18641
  seen.add(path2);
18182
18642
  let mtimeMs = 0;
18183
18643
  try {
18184
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18644
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18185
18645
  } catch {
18186
18646
  mtimeMs = 0;
18187
18647
  }
@@ -18191,16 +18651,16 @@ async function findOrphanWorktrees(repoPaths) {
18191
18651
  }
18192
18652
  try {
18193
18653
  const root = worktreesRoot(repoPath);
18194
- if ((0, import_node_fs46.existsSync)(root)) {
18195
- for (const entry of (0, import_node_fs46.readdirSync)(root, { withFileTypes: true })) {
18654
+ if ((0, import_node_fs47.existsSync)(root)) {
18655
+ for (const entry of (0, import_node_fs47.readdirSync)(root, { withFileTypes: true })) {
18196
18656
  if (!entry.isDirectory()) continue;
18197
- const path2 = (0, import_node_path46.join)(root, entry.name).replace(/\/$/, "");
18657
+ const path2 = (0, import_node_path47.join)(root, entry.name).replace(/\/$/, "");
18198
18658
  if (known.has(path2) || seen.has(path2)) continue;
18199
- if (!(0, import_node_fs46.existsSync)((0, import_node_path46.join)(path2, ".git"))) continue;
18659
+ if (!(0, import_node_fs47.existsSync)((0, import_node_path47.join)(path2, ".git"))) continue;
18200
18660
  seen.add(path2);
18201
18661
  let mtimeMs = 0;
18202
18662
  try {
18203
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18663
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18204
18664
  } catch {
18205
18665
  mtimeMs = Date.now();
18206
18666
  }
@@ -18261,12 +18721,12 @@ function worktreeCleanupSettings() {
18261
18721
  autoCleanupOrphans: a.autoCleanupOrphans
18262
18722
  };
18263
18723
  }
18264
- var import_node_fs46, import_node_path46;
18724
+ var import_node_fs47, import_node_path47;
18265
18725
  var init_orphan_cleanup = __esm({
18266
18726
  "src/git/orphan-cleanup.ts"() {
18267
18727
  "use strict";
18268
- import_node_fs46 = require("fs");
18269
- import_node_path46 = require("path");
18728
+ import_node_fs47 = require("fs");
18729
+ import_node_path47 = require("path");
18270
18730
  init_worktree();
18271
18731
  init_thread_store();
18272
18732
  init_paths();
@@ -18282,6 +18742,7 @@ function stripArchivedTranscript(thread) {
18282
18742
  attachments: [],
18283
18743
  pendingTurnAttachments: [],
18284
18744
  queueAttachments: [],
18745
+ compactedMessages: void 0,
18285
18746
  messages: [historyRetentionStubMessage(thread)]
18286
18747
  },
18287
18748
  { touch: false }
@@ -18441,12 +18902,12 @@ async function cloneRepoIntoSideboard(opts) {
18441
18902
  if (!url) throw new Error("Clone URL is required");
18442
18903
  let name = opts.name?.trim();
18443
18904
  if (!name) {
18444
- const leaf = (0, import_node_path47.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18905
+ const leaf = (0, import_node_path48.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18445
18906
  name = leaf || "repo";
18446
18907
  }
18447
18908
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
18448
- const dest = (0, import_node_path47.join)(sideboardReposDir(), name);
18449
- if ((0, import_node_fs47.existsSync)(dest)) {
18909
+ const dest = (0, import_node_path48.join)(sideboardReposDir(), name);
18910
+ if ((0, import_node_fs48.existsSync)(dest)) {
18450
18911
  const repoPath2 = await resolveRepoRoot(dest);
18451
18912
  const workspace2 = await ensureWorkspace(repoPath2);
18452
18913
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -18461,12 +18922,12 @@ async function cloneRepoIntoSideboard(opts) {
18461
18922
  const workspace = await ensureWorkspace(repoPath);
18462
18923
  return { repoPath, workspace };
18463
18924
  }
18464
- var import_node_fs47, import_node_path47, import_execa6;
18925
+ var import_node_fs48, import_node_path48, import_execa6;
18465
18926
  var init_clone_repo = __esm({
18466
18927
  "src/git/clone-repo.ts"() {
18467
18928
  "use strict";
18468
- import_node_fs47 = require("fs");
18469
- import_node_path47 = require("path");
18929
+ import_node_fs48 = require("fs");
18930
+ import_node_path48 = require("path");
18470
18931
  import_execa6 = require("execa");
18471
18932
  init_paths();
18472
18933
  init_workspaces();
@@ -18524,394 +18985,6 @@ var init_child_halt = __esm({
18524
18985
  }
18525
18986
  });
18526
18987
 
18527
- // src/mcp/wait-for-turn.ts
18528
- function mcpWaitForTurnTimeoutMs(requested) {
18529
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
18530
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
18531
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
18532
- }
18533
- function mcpWaitTaskHint(taskState, status) {
18534
- switch (taskState) {
18535
- case "submitted":
18536
- return MCP_WAIT_QUEUED_HINT;
18537
- case "working":
18538
- return MCP_WAIT_STILL_RUNNING_HINT;
18539
- case "input-required":
18540
- return MCP_WAIT_INPUT_REQUIRED_HINT;
18541
- case "canceled":
18542
- return MCP_WAIT_STOPPED_HINT;
18543
- case "failed":
18544
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
18545
- default:
18546
- return void 0;
18547
- }
18548
- }
18549
- var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT, MCP_WAIT_INPUT_REQUIRED_HINT;
18550
- var init_wait_for_turn = __esm({
18551
- "src/mcp/wait-for-turn.ts"() {
18552
- "use strict";
18553
- MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
18554
- MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
18555
- MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
18556
- 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.";
18557
- MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
18558
- 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.";
18559
- MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
18560
- }
18561
- });
18562
-
18563
- // src/mcp/wait-for-job.ts
18564
- function mcpWaitForJobTimeoutMs(requested) {
18565
- return mcpWaitForTurnTimeoutMs(requested);
18566
- }
18567
- function sanitizeDetachedJobId(id) {
18568
- const s = id.trim();
18569
- if (!JOB_ID_RE.test(s)) {
18570
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
18571
- }
18572
- return s;
18573
- }
18574
- function jobAlive(pid) {
18575
- if (!Number.isInteger(pid) || pid <= 0) return false;
18576
- try {
18577
- process.kill(pid, 0);
18578
- return true;
18579
- } catch {
18580
- return false;
18581
- }
18582
- }
18583
- function processGroupAlive(pgid) {
18584
- if (process.platform === "win32") return false;
18585
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
18586
- try {
18587
- process.kill(-pgid, 0);
18588
- return true;
18589
- } catch (err) {
18590
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
18591
- }
18592
- }
18593
- function jobTreeAlive(pid) {
18594
- if (pid == null) return false;
18595
- return jobAlive(pid) || processGroupAlive(pid);
18596
- }
18597
- function readIntFile(file) {
18598
- if (!(0, import_node_fs48.existsSync)(file)) return null;
18599
- const n = Number.parseInt((0, import_node_fs48.readFileSync)(file, "utf8").trim(), 10);
18600
- return Number.isInteger(n) ? n : null;
18601
- }
18602
- function jobDir(root, id, legacy = false) {
18603
- return (0, import_node_path48.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
18604
- }
18605
- function resolveJobDir(root, id) {
18606
- const modern = jobDir(root, id, false);
18607
- if ((0, import_node_fs48.existsSync)(modern)) return modern;
18608
- const legacy = jobDir(root, id, true);
18609
- if ((0, import_node_fs48.existsSync)(legacy)) return legacy;
18610
- return modern;
18611
- }
18612
- function listJobIdsIn(root, rel) {
18613
- const dir = (0, import_node_path48.join)(root, rel);
18614
- if (!(0, import_node_fs48.existsSync)(dir)) return [];
18615
- return (0, import_node_fs48.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
18616
- }
18617
- function listRunningDetachedJobs(worktreePath) {
18618
- const root = worktreePath.trim();
18619
- if (!root) return [];
18620
- const ids = /* @__PURE__ */ new Set([
18621
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
18622
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
18623
- ]);
18624
- const running = [];
18625
- for (const id of ids) {
18626
- const dir = resolveJobDir(root, id);
18627
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18628
- if (jobTreeAlive(pid)) running.push(id);
18629
- }
18630
- return running.sort();
18631
- }
18632
- function looksLikeDeferredDonePromise(text7) {
18633
- const t = (text7 ?? "").trim();
18634
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
18635
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
18636
- if (!unquoted) return false;
18637
- return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
18638
- }
18639
- function isJobContinuePrompt(text7) {
18640
- if (typeof text7 !== "string") return false;
18641
- const t = text7.trim();
18642
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
18643
- }
18644
- function collapseGhRunWatchLog(text7) {
18645
- if (!text7.includes(GH_WATCH_REFRESH)) return text7;
18646
- const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
18647
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
18648
- if (watchFrames.length <= 1) return text7;
18649
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
18650
- const last = watchFrames[watchFrames.length - 1] ?? "";
18651
- const omitted = watchFrames.length - 1;
18652
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
18653
- `;
18654
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
18655
- }
18656
- function capJobLogDelta(text7) {
18657
- const collapsed = collapseGhRunWatchLog(text7);
18658
- const lines = collapsed.split("\n");
18659
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
18660
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
18661
- if (out.length > MAX_JOB_DELTA_CHARS) {
18662
- out = `\u2026(truncated ${out.length} chars)
18663
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
18664
- }
18665
- return out;
18666
- }
18667
- function formatJobStillRunningContinuePrompt(jobIds) {
18668
- const ids = jobIds.join(", ");
18669
- return [
18670
- `Detached job still running: ${ids}.`,
18671
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
18672
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
18673
- "Then report the result. Do not tell the user you will let them know later."
18674
- ].join(" ");
18675
- }
18676
- function formatDeferredDoneContinuePrompt() {
18677
- return [
18678
- "You ended the turn after promising to report later, but no detached job is running.",
18679
- "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
18680
- "Do not say you will let the user know later."
18681
- ].join(" ");
18682
- }
18683
- function turnWatchedDetachedJob(parts) {
18684
- return parts.some((p) => {
18685
- if (p.type !== "tool") return false;
18686
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
18687
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
18688
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
18689
- });
18690
- }
18691
- function planJobContinue(opts) {
18692
- if (opts.isOrchestrator) return { action: "none" };
18693
- if (opts.agent === "brightsy") return { action: "none" };
18694
- if (opts.queueLength > 0) return { action: "none" };
18695
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
18696
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
18697
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
18698
- return {
18699
- action: "wait",
18700
- jobIds: opts.runningJobIds,
18701
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
18702
- };
18703
- }
18704
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
18705
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
18706
- }
18707
- return { action: "none" };
18708
- }
18709
- function tailProgress(logFile, maxLines = 12) {
18710
- if (!(0, import_node_fs48.existsSync)(logFile)) return "(no log yet)";
18711
- const lines = (0, import_node_fs48.readFileSync)(logFile, "utf8").split("\n");
18712
- return lines.slice(-maxLines).join("\n");
18713
- }
18714
- function lastProgressLine(progress) {
18715
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
18716
- return last ? last.slice(0, 72) : void 0;
18717
- }
18718
- function readLogLines(file) {
18719
- if (!(0, import_node_fs48.existsSync)(file)) return [];
18720
- const lines = (0, import_node_fs48.readFileSync)(file, "utf8").split("\n");
18721
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
18722
- return lines;
18723
- }
18724
- function takeDelta(logFile, cursorFile) {
18725
- const lines = readLogLines(logFile);
18726
- const cursor = readIntFile(cursorFile) ?? 0;
18727
- const start = Math.min(Math.max(0, cursor), lines.length);
18728
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
18729
- }
18730
- function snapshotJob(dir) {
18731
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18732
- const running = jobTreeAlive(pid);
18733
- return {
18734
- pid,
18735
- running,
18736
- exitCode: readIntFile((0, import_node_path48.join)(dir, "exit")),
18737
- log: (0, import_node_path48.join)(dir, "log"),
18738
- cursor: (0, import_node_path48.join)(dir, "present.cursor"),
18739
- progress: tailProgress((0, import_node_path48.join)(dir, "log"))
18740
- };
18741
- }
18742
- function toResult(id, snap, extra) {
18743
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
18744
- const ok = !snap.running && snap.exitCode === 0;
18745
- const stillRunning = snap.running && !ok;
18746
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
18747
- try {
18748
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(snap.cursor), { recursive: true });
18749
- (0, import_node_fs48.writeFileSync)(snap.cursor, `${nextCursor}
18750
- `);
18751
- } catch {
18752
- }
18753
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
18754
- const progress = extra?.progress ?? snap.progress;
18755
- return {
18756
- stillRunning,
18757
- ok,
18758
- failed: Boolean(failed && !stillRunning && !ok),
18759
- status,
18760
- id,
18761
- pid: snap.pid,
18762
- exitCode: snap.exitCode ?? void 0,
18763
- phase: lastProgressLine(progress),
18764
- delta,
18765
- progress,
18766
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
18767
- };
18768
- }
18769
- async function sleepMs(ms) {
18770
- await new Promise((resolve2) => setTimeout(resolve2, ms));
18771
- }
18772
- async function waitForDetachedJob(cwd, id, opts) {
18773
- const jobId = sanitizeDetachedJobId(id);
18774
- const root = cwd.trim() || process.cwd();
18775
- const dir = resolveJobDir(root, jobId);
18776
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
18777
- const sleep2 = opts?.sleep ?? sleepMs;
18778
- if (!(0, import_node_fs48.existsSync)(dir)) {
18779
- return {
18780
- stillRunning: false,
18781
- ok: false,
18782
- failed: true,
18783
- status: "failed",
18784
- id: jobId,
18785
- delta: "",
18786
- progress: "No detached job. Start one first.",
18787
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
18788
- };
18789
- }
18790
- const deadline = Date.now() + timeoutMs;
18791
- let snap = snapshotJob(dir);
18792
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18793
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
18794
- return toResult(jobId, snap, {
18795
- failed: true,
18796
- progress: "No detached job. Start one first."
18797
- });
18798
- }
18799
- while (Date.now() < deadline) {
18800
- snap = snapshotJob(dir);
18801
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18802
- if (!snap.running) return toResult(jobId, snap, { failed: true });
18803
- await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
18804
- }
18805
- snap = snapshotJob(dir);
18806
- return toResult(jobId, snap);
18807
- }
18808
- function killProcessTree(pid, signal) {
18809
- if (!Number.isInteger(pid) || pid <= 0) return;
18810
- if (process.platform !== "win32") {
18811
- try {
18812
- process.kill(-pid, signal);
18813
- return;
18814
- } catch {
18815
- }
18816
- }
18817
- try {
18818
- process.kill(pid, signal);
18819
- } catch {
18820
- }
18821
- }
18822
- function appendJobLog(logFile, line) {
18823
- try {
18824
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(logFile), { recursive: true });
18825
- const body = line.endsWith("\n") ? line : `${line}
18826
- `;
18827
- (0, import_node_fs48.appendFileSync)(logFile, body);
18828
- } catch {
18829
- }
18830
- }
18831
- async function stopDetachedJob(cwd, id, opts) {
18832
- const jobId = sanitizeDetachedJobId(id);
18833
- const root = cwd.trim() || process.cwd();
18834
- const dir = resolveJobDir(root, jobId);
18835
- const sleep2 = opts?.sleep ?? sleepMs;
18836
- const why = opts?.reason?.trim() ?? "";
18837
- if (!(0, import_node_fs48.existsSync)(dir)) {
18838
- return {
18839
- stillRunning: false,
18840
- ok: false,
18841
- failed: true,
18842
- status: "failed",
18843
- id: jobId,
18844
- delta: "",
18845
- progress: "No detached job. Start one first.",
18846
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
18847
- stopped: false,
18848
- reason: "not-found"
18849
- };
18850
- }
18851
- let snap = snapshotJob(dir);
18852
- if (!snap.running) {
18853
- const result2 = toResult(jobId, snap);
18854
- return {
18855
- ...result2,
18856
- stopped: false,
18857
- reason: "not-running",
18858
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
18859
- };
18860
- }
18861
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
18862
- const targetPid = snap.pid;
18863
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
18864
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
18865
- const deadline = Date.now() + graceMs;
18866
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
18867
- await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
18868
- }
18869
- if (targetPid != null) {
18870
- killProcessTree(targetPid, "SIGKILL");
18871
- await sleep2(100);
18872
- }
18873
- const exitFile = (0, import_node_path48.join)(dir, "exit");
18874
- if (!(0, import_node_fs48.existsSync)(exitFile)) {
18875
- try {
18876
- (0, import_node_fs48.writeFileSync)(exitFile, "1\n");
18877
- } catch {
18878
- }
18879
- }
18880
- snap = snapshotJob(dir);
18881
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
18882
- const result = toResult(jobId, snap, { failed: true });
18883
- const stopped = !stillRunning;
18884
- return {
18885
- ...result,
18886
- stillRunning,
18887
- ok: false,
18888
- failed: true,
18889
- status: stillRunning ? "running" : "failed",
18890
- stopped,
18891
- reason: stopped ? "stopped" : "still-running",
18892
- stopReason: why || void 0,
18893
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
18894
- };
18895
- }
18896
- var import_node_fs48, import_node_path48, MAX_JOB_CONTINUES, MAX_JOB_DELTA_LINES, MAX_JOB_DELTA_CHARS, DEFERRED_DONE_MAX_CHARS, GH_WATCH_REFRESH, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
18897
- var init_wait_for_job = __esm({
18898
- "src/mcp/wait-for-job.ts"() {
18899
- "use strict";
18900
- import_node_fs48 = require("fs");
18901
- import_node_path48 = require("path");
18902
- init_workspace_scratch();
18903
- init_wait_for_turn();
18904
- MAX_JOB_CONTINUES = 8;
18905
- MAX_JOB_DELTA_LINES = 80;
18906
- MAX_JOB_DELTA_CHARS = 4e3;
18907
- DEFERRED_DONE_MAX_CHARS = 480;
18908
- GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
18909
- MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
18910
- MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
18911
- JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
18912
- }
18913
- });
18914
-
18915
18988
  // src/detect/detect.ts
18916
18989
  async function detectAgents() {
18917
18990
  ensureAgentPath();
@@ -23694,6 +23767,13 @@ function isPresentPlanToolName(name) {
23694
23767
  if (!name) return false;
23695
23768
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
23696
23769
  }
23770
+ function threadMayPresentPlan(thread) {
23771
+ return Boolean(thread?.planMode);
23772
+ }
23773
+ function shouldRegisterPresentPlan(thread) {
23774
+ if (thread == null) return true;
23775
+ return threadMayPresentPlan(thread);
23776
+ }
23697
23777
  function extractPresentedPlan(parts) {
23698
23778
  if (!parts?.length) return null;
23699
23779
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -23731,7 +23811,7 @@ function resolvePlanMarkdown(opts) {
23731
23811
  }
23732
23812
  return null;
23733
23813
  }
23734
- var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
23814
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
23735
23815
  var init_plan_present = __esm({
23736
23816
  "src/plan/plan-present.ts"() {
23737
23817
  "use strict";
@@ -23739,6 +23819,8 @@ var init_plan_present = __esm({
23739
23819
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
23740
23820
  PLAN_FILE_NAME = "plan.md";
23741
23821
  LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
23822
+ PRESENT_PLAN_TOOL_DESCRIPTION = "Only when Sideboard Plan mode is on (composer Plan chip). Saves the implementation plan to .context/attachments/plan.md and shows Approve / Hand off / Copy in chat. Pass the full plan body in content. Then Claude should call ExitPlanMode. If Plan mode is off, do not call this \u2014 write the plan in chat.";
23823
+ PRESENT_PLAN_REQUIRES_PLAN_MODE = "Plan mode is off. Do not call present_plan or ExitPlanMode. Write the plan in chat. The user can turn Plan on in the composer.";
23742
23824
  }
23743
23825
  });
23744
23826
 
@@ -23748,11 +23830,15 @@ __export(plan_file_exports, {
23748
23830
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
23749
23831
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
23750
23832
  PLAN_FILE_REL: () => PLAN_FILE_REL,
23833
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
23834
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
23751
23835
  extractPresentedPlan: () => extractPresentedPlan,
23752
23836
  isPresentPlanToolName: () => isPresentPlanToolName,
23753
23837
  planFileAbs: () => planFileAbs,
23754
23838
  readPlanFile: () => readPlanFile,
23755
23839
  resolvePlanMarkdown: () => resolvePlanMarkdown,
23840
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
23841
+ threadMayPresentPlan: () => threadMayPresentPlan,
23756
23842
  writePlanFile: () => writePlanFile
23757
23843
  });
23758
23844
  function ensureAttachmentsGitignore(worktreePath) {
@@ -24888,7 +24974,8 @@ var init_orchestrator = __esm({
24888
24974
  if (compact.didCompact) {
24889
24975
  thread = updateThread(threadId, {
24890
24976
  messages: compact.thread.messages,
24891
- sessionId: compact.thread.sessionId
24977
+ sessionId: compact.thread.sessionId,
24978
+ compactedMessages: compact.thread.compactedMessages
24892
24979
  });
24893
24980
  this.emit({
24894
24981
  type: "context_compacted",
@@ -24902,9 +24989,6 @@ var init_orchestrator = __esm({
24902
24989
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
24903
24990
  thread = this.requireThread(threadId);
24904
24991
  }
24905
- if (this.stoppedTurns.has(threadId)) {
24906
- return;
24907
- }
24908
24992
  const promptText = typeof prompt === "string" ? prompt : "";
24909
24993
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
24910
24994
  const parked = takePendingTurnAttachments(thread);
@@ -24921,6 +25005,14 @@ var init_orchestrator = __esm({
24921
25005
  agent: thread.agent,
24922
25006
  orch: isOrchestratorThread(thread)
24923
25007
  });
25008
+ if (this.stoppedTurns.has(threadId)) {
25009
+ const stopped = writeLiveStatus(threadId, "stopped");
25010
+ if (stopped?.status === "stopped") {
25011
+ this.emit({ type: "status_changed", threadId, status: "stopped" });
25012
+ }
25013
+ this.emit({ type: "turn_finished", threadId, exitCode: 1 });
25014
+ return;
25015
+ }
24924
25016
  thread = this.requireThread(threadId);
24925
25017
  const gitPrompt = expandCanonicalGitRequest(promptText);
24926
25018
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -24979,7 +25071,7 @@ var init_orchestrator = __esm({
24979
25071
  ].filter(Boolean).join("\n\n") || void 0;
24980
25072
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
24981
25073
  const agentPrompt = [
24982
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
25074
+ planModeTurnInstruction(thread.planMode),
24983
25075
  orchestrationReminder,
24984
25076
  prGateDirective,
24985
25077
  injectedNoticeContext,
@@ -25482,6 +25574,9 @@ var init_orchestrator = __esm({
25482
25574
  }
25483
25575
  const preferredPorts = active?.ports?.length ? active.ports : active?.port != null ? [active.port] : void 0;
25484
25576
  if (active && !existing) {
25577
+ if (typeof active.pid === "number") {
25578
+ killDetachedRunTree(active.pid, thread.worktreePath);
25579
+ }
25485
25580
  killListenersOnPorts(collectActiveRunPorts([active]));
25486
25581
  this.syncWorktreeRuns(
25487
25582
  thread.worktreePath,
@@ -25554,7 +25649,8 @@ var init_orchestrator = __esm({
25554
25649
  scriptName: resolvedName,
25555
25650
  port: handle.port,
25556
25651
  ports: handle.ports,
25557
- startedAt
25652
+ startedAt,
25653
+ ...typeof handle.pid === "number" && handle.pid > 1 ? { pid: handle.pid } : {}
25558
25654
  };
25559
25655
  const nextRuns = [
25560
25656
  ...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
@@ -25700,6 +25796,9 @@ var init_orchestrator = __esm({
25700
25796
  }
25701
25797
  const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
25702
25798
  if (!hadHandle2 && run2) {
25799
+ if (typeof run2.pid === "number") {
25800
+ killDetachedRunTree(run2.pid, thread.worktreePath);
25801
+ }
25703
25802
  killListenersOnPorts(collectActiveRunPorts([run2]));
25704
25803
  }
25705
25804
  const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
@@ -25721,6 +25820,11 @@ var init_orchestrator = __esm({
25721
25820
  }
25722
25821
  }
25723
25822
  if (!hadHandle && shared.activeRuns.length > 0) {
25823
+ for (const run2 of shared.activeRuns) {
25824
+ if (typeof run2.pid === "number") {
25825
+ killDetachedRunTree(run2.pid, thread.worktreePath);
25826
+ }
25827
+ }
25724
25828
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
25725
25829
  }
25726
25830
  this.syncWorktreeRuns(thread.worktreePath, [], null);
@@ -25762,6 +25866,11 @@ var init_orchestrator = __esm({
25762
25866
  (k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
25763
25867
  );
25764
25868
  if (!force && hasLiveHandle) continue;
25869
+ for (const run2 of shared.activeRuns) {
25870
+ if (typeof run2.pid === "number") {
25871
+ killDetachedRunTree(run2.pid, thread.worktreePath);
25872
+ }
25873
+ }
25765
25874
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
25766
25875
  if (shared.devPort != null) {
25767
25876
  killListenersOnPorts([shared.devPort]);
@@ -26896,8 +27005,11 @@ __export(index_exports, {
26896
27005
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
26897
27006
  PLAN_FILE_REL: () => PLAN_FILE_REL,
26898
27007
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
27008
+ PLAN_MODE_OFF_INSTRUCTION: () => PLAN_MODE_OFF_INSTRUCTION,
26899
27009
  PLAN_QUESTION_ANSWERS_PREFIX: () => PLAN_QUESTION_ANSWERS_PREFIX,
26900
27010
  PORT_RANGE_SIZE: () => PORT_RANGE_SIZE,
27011
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
27012
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
26901
27013
  PROFILE_NOTES_MAX: () => PROFILE_NOTES_MAX,
26902
27014
  PR_DETAILS_JSON_FIELDS: () => PR_DETAILS_JSON_FIELDS,
26903
27015
  REPO_REVIEW_NAME: () => REPO_REVIEW_NAME,
@@ -26968,6 +27080,7 @@ __export(index_exports, {
26968
27080
  annotateDevAccessRuns: () => annotateDevAccessRuns,
26969
27081
  appDataDir: () => appDataDir,
26970
27082
  appSettingsPath: () => appSettingsPath,
27083
+ appendCompactedMessages: () => appendCompactedMessages,
26971
27084
  appendIndexedGitConfig: () => appendIndexedGitConfig,
26972
27085
  appendMessage: () => appendMessage,
26973
27086
  appendQueuedItem: () => appendQueuedItem,
@@ -27055,6 +27168,9 @@ __export(index_exports, {
27055
27168
  commentGitHubIssue: () => commentGitHubIssue,
27056
27169
  commentLinearIssue: () => commentLinearIssue,
27057
27170
  commitAll: () => commitAll,
27171
+ compactArchiveProse: () => compactArchiveProse,
27172
+ compactPartsForSummaryMessage: () => compactPartsForSummaryMessage,
27173
+ compactSummaryPart: () => compactSummaryPart,
27058
27174
  computeNextRunAt: () => computeNextRunAt,
27059
27175
  conductorBundledBinDir: () => conductorBundledBinDir,
27060
27176
  conductorDbPath: () => conductorDbPath,
@@ -27361,6 +27477,7 @@ __export(index_exports, {
27361
27477
  isTerminalTaskState: () => isTerminalTaskState,
27362
27478
  isThinkingEffort: () => isThinkingEffort,
27363
27479
  isThisProcessDesktopHost: () => isThisProcessDesktopHost,
27480
+ isThisWorktreeListenerCwd: () => isThisWorktreeListenerCwd,
27364
27481
  isThreadCaffeinated: () => isThreadCaffeinated,
27365
27482
  isThreadRecordFile: () => isThreadRecordFile,
27366
27483
  isUnclaimedRunScriptRequest: () => isUnclaimedRunScriptRequest,
@@ -27370,6 +27487,7 @@ __export(index_exports, {
27370
27487
  issueMatchesAssignee: () => issueMatchesAssignee,
27371
27488
  issueSourceLabel: () => issueSourceLabel,
27372
27489
  issueTicketFromThread: () => issueTicketFromThread,
27490
+ killDetachedRunTree: () => killDetachedRunTree,
27373
27491
  killListenersOnPorts: () => killListenersOnPorts,
27374
27492
  killStaleWorktreeListeners: () => killStaleWorktreeListeners,
27375
27493
  killableListenerPids: () => killableListenerPids,
@@ -27452,6 +27570,7 @@ __export(index_exports, {
27452
27570
  mergeSideboardIntoMcpServersJson: () => mergeSideboardIntoMcpServersJson,
27453
27571
  mergeUsage: () => mergeUsage,
27454
27572
  messagePartParentId: () => messagePartParentId,
27573
+ messagesForCompactArchive: () => messagesForCompactArchive,
27455
27574
  messagesSinceLastBrightsyContextSummary: () => messagesSinceLastBrightsyContextSummary,
27456
27575
  moveQueuedItemToFront: () => moveQueuedItemToFront,
27457
27576
  needsCoordinatorAction: () => needsCoordinatorAction,
@@ -27511,6 +27630,7 @@ __export(index_exports, {
27511
27630
  planFileAbs: () => planFileAbs,
27512
27631
  planHistoryAgePurge: () => planHistoryAgePurge,
27513
27632
  planHistoryRetention: () => planHistoryRetention,
27633
+ planModeTurnInstruction: () => planModeTurnInstruction,
27514
27634
  planQuestionsSignature: () => planQuestionsSignature,
27515
27635
  pnpmFilterPackage: () => pnpmFilterPackage,
27516
27636
  pollSlackOutboundWatches: () => pollSlackOutboundWatches,
@@ -27638,6 +27758,7 @@ __export(index_exports, {
27638
27758
  shouldInjectBrightsyMcp: () => shouldInjectBrightsyMcp,
27639
27759
  shouldNotifyOrchestrator: () => shouldNotifyOrchestrator,
27640
27760
  shouldRefreshReviewRequestTemplate: () => shouldRefreshReviewRequestTemplate,
27761
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
27641
27762
  shouldRemoveWorktreeOnTeardown: () => shouldRemoveWorktreeOnTeardown,
27642
27763
  shouldResetSessionForOccupancy: () => shouldResetSessionForOccupancy,
27643
27764
  shouldRunHistoryCleanup: () => shouldRunHistoryCleanup,
@@ -27699,6 +27820,7 @@ __export(index_exports, {
27699
27820
  threadHasCompactedContext: () => threadHasCompactedContext,
27700
27821
  threadLivePath: () => threadLivePath,
27701
27822
  threadLockPath: () => threadLockPath,
27823
+ threadMayPresentPlan: () => threadMayPresentPlan,
27702
27824
  threadRequestsBrightsyMcp: () => threadRequestsBrightsyMcp,
27703
27825
  threadsDir: () => threadsDir,
27704
27826
  threadsSharingWorktree: () => threadsSharingWorktree,
@@ -27872,7 +27994,8 @@ function slimThreadForUiList(thread) {
27872
27994
  attachments: [],
27873
27995
  pendingTurnAttachments: [],
27874
27996
  queueAttachments: [],
27875
- messages: slimMessagesForUiList(messages, thread.updatedAt)
27997
+ messages: slimMessagesForUiList(messages, thread.updatedAt),
27998
+ compactedMessages: void 0
27876
27999
  };
27877
28000
  }
27878
28001
  function slimMessagesForUiList(messages, fallbackTs) {
@@ -30426,6 +30549,7 @@ function mcpArchiveBlockedReason(thread) {
30426
30549
 
30427
30550
  // src/mcp/server.ts
30428
30551
  init_profile();
30552
+ init_plan_present();
30429
30553
 
30430
30554
  // src/mcp/run-script-ref.ts
30431
30555
  init_chat_tabs();
@@ -33338,31 +33462,40 @@ async function startMcpServer() {
33338
33462
  return mcpJson(payload);
33339
33463
  }
33340
33464
  );
33341
- server.tool(
33342
- "present_plan",
33343
- "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
33344
- {
33345
- title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
33346
- content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
33347
- thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
33348
- },
33349
- async ({ title, content, thread_id }) => {
33350
- const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
33351
- let root = process.cwd();
33352
- if (thread_id?.trim()) {
33353
- const t = orch.getThread(thread_id.trim());
33465
+ const callingThreadId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
33466
+ const callingThread = callingThreadId ? orch.getThread(callingThreadId) : null;
33467
+ if (shouldRegisterPresentPlan(callingThread)) {
33468
+ server.tool(
33469
+ "present_plan",
33470
+ PRESENT_PLAN_TOOL_DESCRIPTION,
33471
+ {
33472
+ title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
33473
+ content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
33474
+ thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
33475
+ },
33476
+ async ({ title, content, thread_id }) => {
33477
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
33478
+ const ref = thread_id?.trim() || callingThreadId;
33479
+ const t = ref ? orch.getThread(ref) : null;
33480
+ if (t && !threadMayPresentPlan(t)) {
33481
+ return mcpJson(
33482
+ { ok: false, error: PRESENT_PLAN_REQUIRES_PLAN_MODE },
33483
+ true
33484
+ );
33485
+ }
33486
+ let root = process.cwd();
33354
33487
  if (t?.worktreePath?.trim()) root = t.worktreePath;
33488
+ const path2 = writePlanFile2(root, content);
33489
+ const payload = {
33490
+ ok: true,
33491
+ path: path2,
33492
+ title: title?.trim() || "Plan",
33493
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
33494
+ };
33495
+ return mcpJson(payload);
33355
33496
  }
33356
- const path2 = writePlanFile2(root, content);
33357
- const payload = {
33358
- ok: true,
33359
- path: path2,
33360
- title: title?.trim() || "Plan",
33361
- message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
33362
- };
33363
- return mcpJson(payload);
33364
- }
33365
- );
33497
+ );
33498
+ }
33366
33499
  server.tool(
33367
33500
  "present_schema",
33368
33501
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
@@ -36699,8 +36832,11 @@ async function startSlackRelayServer(opts) {
36699
36832
  PLAN_FILE_NAME,
36700
36833
  PLAN_FILE_REL,
36701
36834
  PLAN_MODE_INSTRUCTION,
36835
+ PLAN_MODE_OFF_INSTRUCTION,
36702
36836
  PLAN_QUESTION_ANSWERS_PREFIX,
36703
36837
  PORT_RANGE_SIZE,
36838
+ PRESENT_PLAN_REQUIRES_PLAN_MODE,
36839
+ PRESENT_PLAN_TOOL_DESCRIPTION,
36704
36840
  PROFILE_NOTES_MAX,
36705
36841
  PR_DETAILS_JSON_FIELDS,
36706
36842
  REPO_REVIEW_NAME,
@@ -36771,6 +36907,7 @@ async function startSlackRelayServer(opts) {
36771
36907
  annotateDevAccessRuns,
36772
36908
  appDataDir,
36773
36909
  appSettingsPath,
36910
+ appendCompactedMessages,
36774
36911
  appendIndexedGitConfig,
36775
36912
  appendMessage,
36776
36913
  appendQueuedItem,
@@ -36858,6 +36995,9 @@ async function startSlackRelayServer(opts) {
36858
36995
  commentGitHubIssue,
36859
36996
  commentLinearIssue,
36860
36997
  commitAll,
36998
+ compactArchiveProse,
36999
+ compactPartsForSummaryMessage,
37000
+ compactSummaryPart,
36861
37001
  computeNextRunAt,
36862
37002
  conductorBundledBinDir,
36863
37003
  conductorDbPath,
@@ -37164,6 +37304,7 @@ async function startSlackRelayServer(opts) {
37164
37304
  isTerminalTaskState,
37165
37305
  isThinkingEffort,
37166
37306
  isThisProcessDesktopHost,
37307
+ isThisWorktreeListenerCwd,
37167
37308
  isThreadCaffeinated,
37168
37309
  isThreadRecordFile,
37169
37310
  isUnclaimedRunScriptRequest,
@@ -37173,6 +37314,7 @@ async function startSlackRelayServer(opts) {
37173
37314
  issueMatchesAssignee,
37174
37315
  issueSourceLabel,
37175
37316
  issueTicketFromThread,
37317
+ killDetachedRunTree,
37176
37318
  killListenersOnPorts,
37177
37319
  killStaleWorktreeListeners,
37178
37320
  killableListenerPids,
@@ -37255,6 +37397,7 @@ async function startSlackRelayServer(opts) {
37255
37397
  mergeSideboardIntoMcpServersJson,
37256
37398
  mergeUsage,
37257
37399
  messagePartParentId,
37400
+ messagesForCompactArchive,
37258
37401
  messagesSinceLastBrightsyContextSummary,
37259
37402
  moveQueuedItemToFront,
37260
37403
  needsCoordinatorAction,
@@ -37314,6 +37457,7 @@ async function startSlackRelayServer(opts) {
37314
37457
  planFileAbs,
37315
37458
  planHistoryAgePurge,
37316
37459
  planHistoryRetention,
37460
+ planModeTurnInstruction,
37317
37461
  planQuestionsSignature,
37318
37462
  pnpmFilterPackage,
37319
37463
  pollSlackOutboundWatches,
@@ -37441,6 +37585,7 @@ async function startSlackRelayServer(opts) {
37441
37585
  shouldInjectBrightsyMcp,
37442
37586
  shouldNotifyOrchestrator,
37443
37587
  shouldRefreshReviewRequestTemplate,
37588
+ shouldRegisterPresentPlan,
37444
37589
  shouldRemoveWorktreeOnTeardown,
37445
37590
  shouldResetSessionForOccupancy,
37446
37591
  shouldRunHistoryCleanup,
@@ -37502,6 +37647,7 @@ async function startSlackRelayServer(opts) {
37502
37647
  threadHasCompactedContext,
37503
37648
  threadLivePath,
37504
37649
  threadLockPath,
37650
+ threadMayPresentPlan,
37505
37651
  threadRequestsBrightsyMcp,
37506
37652
  threadsDir,
37507
37653
  threadsSharingWorktree,