@sideboard-ai/core 0.1.242 → 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-LRMJ6V6U.js → chunk-DARSRQUE.js} +1503 -1462
  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-R3YKDTVE.js → chunk-NEOAHPMK.js} +1569 -1526
  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 +574 -464
  37. package/dist/index.d.cts +43 -3
  38. package/dist/index.d.ts +43 -3
  39. package/dist/index.js +70 -38
  40. package/dist/mcp/run-stdio.cjs +1633 -1544
  41. package/dist/mcp/run-stdio.js +50 -36
  42. package/dist/{orchestrator-66ZFQHW5.js → orchestrator-3IOISDFS.js} +7 -7
  43. package/dist/{orchestrator-VYNPNHY4.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) {
@@ -17732,12 +18172,16 @@ function signalPid(pid, signal) {
17732
18172
  function isThisWorktreeListenerCwd(cwd, worktreePath) {
17733
18173
  const wt = realpathOrResolve(worktreePath);
17734
18174
  const c = realpathOrResolve(cwd);
17735
- return c === wt || c.startsWith(wt + import_node_path45.sep);
18175
+ return c === wt || c.startsWith(wt + import_node_path46.sep);
17736
18176
  }
17737
18177
  function killDetachedRunTree(pid, worktreePath) {
17738
18178
  if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
17739
18179
  const cwd = listenerCwd(pid);
17740
- if (!cwd || !isThisWorktreeListenerCwd(cwd, worktreePath)) return;
18180
+ if (cwd) {
18181
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
18182
+ } else if (!processGroupAlive(pid)) {
18183
+ return;
18184
+ }
17741
18185
  try {
17742
18186
  process.kill(-pid, "SIGTERM");
17743
18187
  } catch {
@@ -18134,14 +18578,14 @@ async function startDevServer(repoPath, worktreePath, onLine, opts) {
18134
18578
  throw err;
18135
18579
  }
18136
18580
  }
18137
- 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;
18138
18582
  var init_conductor = __esm({
18139
18583
  "src/hook/conductor.ts"() {
18140
18584
  "use strict";
18141
- import_node_fs45 = require("fs");
18585
+ import_node_fs46 = require("fs");
18142
18586
  import_node_child_process5 = require("child_process");
18143
18587
  import_node_net = require("net");
18144
- import_node_path45 = require("path");
18588
+ import_node_path46 = require("path");
18145
18589
  import_execa4 = require("execa");
18146
18590
  import_node_readline3 = require("readline");
18147
18591
  init_settings();
@@ -18153,6 +18597,7 @@ var init_conductor = __esm({
18153
18597
  init_request_review();
18154
18598
  init_worktree_exclude();
18155
18599
  init_worktree_run_ports();
18600
+ init_wait_for_job();
18156
18601
  init_worktree_run_ports();
18157
18602
  init_nested_electron_env();
18158
18603
  PORT_RANGE_SIZE = 10;
@@ -18173,9 +18618,9 @@ async function findOrphanWorktrees(repoPaths) {
18173
18618
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
18174
18619
  );
18175
18620
  const homeRoot = sideboardWorkspacesDir();
18176
- if ((0, import_node_fs46.existsSync)(homeRoot)) {
18621
+ if ((0, import_node_fs47.existsSync)(homeRoot)) {
18177
18622
  try {
18178
- 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 })) {
18179
18624
  if (!entry.isDirectory()) continue;
18180
18625
  void entry;
18181
18626
  }
@@ -18185,7 +18630,7 @@ async function findOrphanWorktrees(repoPaths) {
18185
18630
  const orphans = [];
18186
18631
  const seen = /* @__PURE__ */ new Set();
18187
18632
  for (const repoPath of repos) {
18188
- if (!repoPath || !(0, import_node_fs46.existsSync)(repoPath)) continue;
18633
+ if (!repoPath || !(0, import_node_fs47.existsSync)(repoPath)) continue;
18189
18634
  try {
18190
18635
  const wts = await listWorktrees(repoPath);
18191
18636
  for (const wt of wts) {
@@ -18196,7 +18641,7 @@ async function findOrphanWorktrees(repoPaths) {
18196
18641
  seen.add(path2);
18197
18642
  let mtimeMs = 0;
18198
18643
  try {
18199
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18644
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18200
18645
  } catch {
18201
18646
  mtimeMs = 0;
18202
18647
  }
@@ -18206,16 +18651,16 @@ async function findOrphanWorktrees(repoPaths) {
18206
18651
  }
18207
18652
  try {
18208
18653
  const root = worktreesRoot(repoPath);
18209
- if ((0, import_node_fs46.existsSync)(root)) {
18210
- 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 })) {
18211
18656
  if (!entry.isDirectory()) continue;
18212
- const path2 = (0, import_node_path46.join)(root, entry.name).replace(/\/$/, "");
18657
+ const path2 = (0, import_node_path47.join)(root, entry.name).replace(/\/$/, "");
18213
18658
  if (known.has(path2) || seen.has(path2)) continue;
18214
- 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;
18215
18660
  seen.add(path2);
18216
18661
  let mtimeMs = 0;
18217
18662
  try {
18218
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18663
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18219
18664
  } catch {
18220
18665
  mtimeMs = Date.now();
18221
18666
  }
@@ -18276,12 +18721,12 @@ function worktreeCleanupSettings() {
18276
18721
  autoCleanupOrphans: a.autoCleanupOrphans
18277
18722
  };
18278
18723
  }
18279
- var import_node_fs46, import_node_path46;
18724
+ var import_node_fs47, import_node_path47;
18280
18725
  var init_orphan_cleanup = __esm({
18281
18726
  "src/git/orphan-cleanup.ts"() {
18282
18727
  "use strict";
18283
- import_node_fs46 = require("fs");
18284
- import_node_path46 = require("path");
18728
+ import_node_fs47 = require("fs");
18729
+ import_node_path47 = require("path");
18285
18730
  init_worktree();
18286
18731
  init_thread_store();
18287
18732
  init_paths();
@@ -18297,6 +18742,7 @@ function stripArchivedTranscript(thread) {
18297
18742
  attachments: [],
18298
18743
  pendingTurnAttachments: [],
18299
18744
  queueAttachments: [],
18745
+ compactedMessages: void 0,
18300
18746
  messages: [historyRetentionStubMessage(thread)]
18301
18747
  },
18302
18748
  { touch: false }
@@ -18456,12 +18902,12 @@ async function cloneRepoIntoSideboard(opts) {
18456
18902
  if (!url) throw new Error("Clone URL is required");
18457
18903
  let name = opts.name?.trim();
18458
18904
  if (!name) {
18459
- const leaf = (0, import_node_path47.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18905
+ const leaf = (0, import_node_path48.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18460
18906
  name = leaf || "repo";
18461
18907
  }
18462
18908
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
18463
- const dest = (0, import_node_path47.join)(sideboardReposDir(), name);
18464
- 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)) {
18465
18911
  const repoPath2 = await resolveRepoRoot(dest);
18466
18912
  const workspace2 = await ensureWorkspace(repoPath2);
18467
18913
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -18476,12 +18922,12 @@ async function cloneRepoIntoSideboard(opts) {
18476
18922
  const workspace = await ensureWorkspace(repoPath);
18477
18923
  return { repoPath, workspace };
18478
18924
  }
18479
- var import_node_fs47, import_node_path47, import_execa6;
18925
+ var import_node_fs48, import_node_path48, import_execa6;
18480
18926
  var init_clone_repo = __esm({
18481
18927
  "src/git/clone-repo.ts"() {
18482
18928
  "use strict";
18483
- import_node_fs47 = require("fs");
18484
- import_node_path47 = require("path");
18929
+ import_node_fs48 = require("fs");
18930
+ import_node_path48 = require("path");
18485
18931
  import_execa6 = require("execa");
18486
18932
  init_paths();
18487
18933
  init_workspaces();
@@ -18539,394 +18985,6 @@ var init_child_halt = __esm({
18539
18985
  }
18540
18986
  });
18541
18987
 
18542
- // src/mcp/wait-for-turn.ts
18543
- function mcpWaitForTurnTimeoutMs(requested) {
18544
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
18545
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
18546
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
18547
- }
18548
- function mcpWaitTaskHint(taskState, status) {
18549
- switch (taskState) {
18550
- case "submitted":
18551
- return MCP_WAIT_QUEUED_HINT;
18552
- case "working":
18553
- return MCP_WAIT_STILL_RUNNING_HINT;
18554
- case "input-required":
18555
- return MCP_WAIT_INPUT_REQUIRED_HINT;
18556
- case "canceled":
18557
- return MCP_WAIT_STOPPED_HINT;
18558
- case "failed":
18559
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
18560
- default:
18561
- return void 0;
18562
- }
18563
- }
18564
- 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;
18565
- var init_wait_for_turn = __esm({
18566
- "src/mcp/wait-for-turn.ts"() {
18567
- "use strict";
18568
- MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
18569
- 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.";
18570
- 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.";
18571
- 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.";
18572
- MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
18573
- 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.";
18574
- 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.";
18575
- }
18576
- });
18577
-
18578
- // src/mcp/wait-for-job.ts
18579
- function mcpWaitForJobTimeoutMs(requested) {
18580
- return mcpWaitForTurnTimeoutMs(requested);
18581
- }
18582
- function sanitizeDetachedJobId(id) {
18583
- const s = id.trim();
18584
- if (!JOB_ID_RE.test(s)) {
18585
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
18586
- }
18587
- return s;
18588
- }
18589
- function jobAlive(pid) {
18590
- if (!Number.isInteger(pid) || pid <= 0) return false;
18591
- try {
18592
- process.kill(pid, 0);
18593
- return true;
18594
- } catch {
18595
- return false;
18596
- }
18597
- }
18598
- function processGroupAlive(pgid) {
18599
- if (process.platform === "win32") return false;
18600
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
18601
- try {
18602
- process.kill(-pgid, 0);
18603
- return true;
18604
- } catch (err) {
18605
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
18606
- }
18607
- }
18608
- function jobTreeAlive(pid) {
18609
- if (pid == null) return false;
18610
- return jobAlive(pid) || processGroupAlive(pid);
18611
- }
18612
- function readIntFile(file) {
18613
- if (!(0, import_node_fs48.existsSync)(file)) return null;
18614
- const n = Number.parseInt((0, import_node_fs48.readFileSync)(file, "utf8").trim(), 10);
18615
- return Number.isInteger(n) ? n : null;
18616
- }
18617
- function jobDir(root, id, legacy = false) {
18618
- return (0, import_node_path48.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
18619
- }
18620
- function resolveJobDir(root, id) {
18621
- const modern = jobDir(root, id, false);
18622
- if ((0, import_node_fs48.existsSync)(modern)) return modern;
18623
- const legacy = jobDir(root, id, true);
18624
- if ((0, import_node_fs48.existsSync)(legacy)) return legacy;
18625
- return modern;
18626
- }
18627
- function listJobIdsIn(root, rel) {
18628
- const dir = (0, import_node_path48.join)(root, rel);
18629
- if (!(0, import_node_fs48.existsSync)(dir)) return [];
18630
- return (0, import_node_fs48.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
18631
- }
18632
- function listRunningDetachedJobs(worktreePath) {
18633
- const root = worktreePath.trim();
18634
- if (!root) return [];
18635
- const ids = /* @__PURE__ */ new Set([
18636
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
18637
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
18638
- ]);
18639
- const running = [];
18640
- for (const id of ids) {
18641
- const dir = resolveJobDir(root, id);
18642
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18643
- if (jobTreeAlive(pid)) running.push(id);
18644
- }
18645
- return running.sort();
18646
- }
18647
- function looksLikeDeferredDonePromise(text7) {
18648
- const t = (text7 ?? "").trim();
18649
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
18650
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
18651
- if (!unquoted) return false;
18652
- 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);
18653
- }
18654
- function isJobContinuePrompt(text7) {
18655
- if (typeof text7 !== "string") return false;
18656
- const t = text7.trim();
18657
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
18658
- }
18659
- function collapseGhRunWatchLog(text7) {
18660
- if (!text7.includes(GH_WATCH_REFRESH)) return text7;
18661
- const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
18662
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
18663
- if (watchFrames.length <= 1) return text7;
18664
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
18665
- const last = watchFrames[watchFrames.length - 1] ?? "";
18666
- const omitted = watchFrames.length - 1;
18667
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
18668
- `;
18669
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
18670
- }
18671
- function capJobLogDelta(text7) {
18672
- const collapsed = collapseGhRunWatchLog(text7);
18673
- const lines = collapsed.split("\n");
18674
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
18675
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
18676
- if (out.length > MAX_JOB_DELTA_CHARS) {
18677
- out = `\u2026(truncated ${out.length} chars)
18678
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
18679
- }
18680
- return out;
18681
- }
18682
- function formatJobStillRunningContinuePrompt(jobIds) {
18683
- const ids = jobIds.join(", ");
18684
- return [
18685
- `Detached job still running: ${ids}.`,
18686
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
18687
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
18688
- "Then report the result. Do not tell the user you will let them know later."
18689
- ].join(" ");
18690
- }
18691
- function formatDeferredDoneContinuePrompt() {
18692
- return [
18693
- "You ended the turn after promising to report later, but no detached job is running.",
18694
- "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).",
18695
- "Do not say you will let the user know later."
18696
- ].join(" ");
18697
- }
18698
- function turnWatchedDetachedJob(parts) {
18699
- return parts.some((p) => {
18700
- if (p.type !== "tool") return false;
18701
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
18702
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
18703
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
18704
- });
18705
- }
18706
- function planJobContinue(opts) {
18707
- if (opts.isOrchestrator) return { action: "none" };
18708
- if (opts.agent === "brightsy") return { action: "none" };
18709
- if (opts.queueLength > 0) return { action: "none" };
18710
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
18711
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
18712
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
18713
- return {
18714
- action: "wait",
18715
- jobIds: opts.runningJobIds,
18716
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
18717
- };
18718
- }
18719
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
18720
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
18721
- }
18722
- return { action: "none" };
18723
- }
18724
- function tailProgress(logFile, maxLines = 12) {
18725
- if (!(0, import_node_fs48.existsSync)(logFile)) return "(no log yet)";
18726
- const lines = (0, import_node_fs48.readFileSync)(logFile, "utf8").split("\n");
18727
- return lines.slice(-maxLines).join("\n");
18728
- }
18729
- function lastProgressLine(progress) {
18730
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
18731
- return last ? last.slice(0, 72) : void 0;
18732
- }
18733
- function readLogLines(file) {
18734
- if (!(0, import_node_fs48.existsSync)(file)) return [];
18735
- const lines = (0, import_node_fs48.readFileSync)(file, "utf8").split("\n");
18736
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
18737
- return lines;
18738
- }
18739
- function takeDelta(logFile, cursorFile) {
18740
- const lines = readLogLines(logFile);
18741
- const cursor = readIntFile(cursorFile) ?? 0;
18742
- const start = Math.min(Math.max(0, cursor), lines.length);
18743
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
18744
- }
18745
- function snapshotJob(dir) {
18746
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18747
- const running = jobTreeAlive(pid);
18748
- return {
18749
- pid,
18750
- running,
18751
- exitCode: readIntFile((0, import_node_path48.join)(dir, "exit")),
18752
- log: (0, import_node_path48.join)(dir, "log"),
18753
- cursor: (0, import_node_path48.join)(dir, "present.cursor"),
18754
- progress: tailProgress((0, import_node_path48.join)(dir, "log"))
18755
- };
18756
- }
18757
- function toResult(id, snap, extra) {
18758
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
18759
- const ok = !snap.running && snap.exitCode === 0;
18760
- const stillRunning = snap.running && !ok;
18761
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
18762
- try {
18763
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(snap.cursor), { recursive: true });
18764
- (0, import_node_fs48.writeFileSync)(snap.cursor, `${nextCursor}
18765
- `);
18766
- } catch {
18767
- }
18768
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
18769
- const progress = extra?.progress ?? snap.progress;
18770
- return {
18771
- stillRunning,
18772
- ok,
18773
- failed: Boolean(failed && !stillRunning && !ok),
18774
- status,
18775
- id,
18776
- pid: snap.pid,
18777
- exitCode: snap.exitCode ?? void 0,
18778
- phase: lastProgressLine(progress),
18779
- delta,
18780
- progress,
18781
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
18782
- };
18783
- }
18784
- async function sleepMs(ms) {
18785
- await new Promise((resolve2) => setTimeout(resolve2, ms));
18786
- }
18787
- async function waitForDetachedJob(cwd, id, opts) {
18788
- const jobId = sanitizeDetachedJobId(id);
18789
- const root = cwd.trim() || process.cwd();
18790
- const dir = resolveJobDir(root, jobId);
18791
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
18792
- const sleep2 = opts?.sleep ?? sleepMs;
18793
- if (!(0, import_node_fs48.existsSync)(dir)) {
18794
- return {
18795
- stillRunning: false,
18796
- ok: false,
18797
- failed: true,
18798
- status: "failed",
18799
- id: jobId,
18800
- delta: "",
18801
- progress: "No detached job. Start one first.",
18802
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
18803
- };
18804
- }
18805
- const deadline = Date.now() + timeoutMs;
18806
- let snap = snapshotJob(dir);
18807
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18808
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
18809
- return toResult(jobId, snap, {
18810
- failed: true,
18811
- progress: "No detached job. Start one first."
18812
- });
18813
- }
18814
- while (Date.now() < deadline) {
18815
- snap = snapshotJob(dir);
18816
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18817
- if (!snap.running) return toResult(jobId, snap, { failed: true });
18818
- await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
18819
- }
18820
- snap = snapshotJob(dir);
18821
- return toResult(jobId, snap);
18822
- }
18823
- function killProcessTree(pid, signal) {
18824
- if (!Number.isInteger(pid) || pid <= 0) return;
18825
- if (process.platform !== "win32") {
18826
- try {
18827
- process.kill(-pid, signal);
18828
- return;
18829
- } catch {
18830
- }
18831
- }
18832
- try {
18833
- process.kill(pid, signal);
18834
- } catch {
18835
- }
18836
- }
18837
- function appendJobLog(logFile, line) {
18838
- try {
18839
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(logFile), { recursive: true });
18840
- const body = line.endsWith("\n") ? line : `${line}
18841
- `;
18842
- (0, import_node_fs48.appendFileSync)(logFile, body);
18843
- } catch {
18844
- }
18845
- }
18846
- async function stopDetachedJob(cwd, id, opts) {
18847
- const jobId = sanitizeDetachedJobId(id);
18848
- const root = cwd.trim() || process.cwd();
18849
- const dir = resolveJobDir(root, jobId);
18850
- const sleep2 = opts?.sleep ?? sleepMs;
18851
- const why = opts?.reason?.trim() ?? "";
18852
- if (!(0, import_node_fs48.existsSync)(dir)) {
18853
- return {
18854
- stillRunning: false,
18855
- ok: false,
18856
- failed: true,
18857
- status: "failed",
18858
- id: jobId,
18859
- delta: "",
18860
- progress: "No detached job. Start one first.",
18861
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
18862
- stopped: false,
18863
- reason: "not-found"
18864
- };
18865
- }
18866
- let snap = snapshotJob(dir);
18867
- if (!snap.running) {
18868
- const result2 = toResult(jobId, snap);
18869
- return {
18870
- ...result2,
18871
- stopped: false,
18872
- reason: "not-running",
18873
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
18874
- };
18875
- }
18876
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
18877
- const targetPid = snap.pid;
18878
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
18879
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
18880
- const deadline = Date.now() + graceMs;
18881
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
18882
- await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
18883
- }
18884
- if (targetPid != null) {
18885
- killProcessTree(targetPid, "SIGKILL");
18886
- await sleep2(100);
18887
- }
18888
- const exitFile = (0, import_node_path48.join)(dir, "exit");
18889
- if (!(0, import_node_fs48.existsSync)(exitFile)) {
18890
- try {
18891
- (0, import_node_fs48.writeFileSync)(exitFile, "1\n");
18892
- } catch {
18893
- }
18894
- }
18895
- snap = snapshotJob(dir);
18896
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
18897
- const result = toResult(jobId, snap, { failed: true });
18898
- const stopped = !stillRunning;
18899
- return {
18900
- ...result,
18901
- stillRunning,
18902
- ok: false,
18903
- failed: true,
18904
- status: stillRunning ? "running" : "failed",
18905
- stopped,
18906
- reason: stopped ? "stopped" : "still-running",
18907
- stopReason: why || void 0,
18908
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
18909
- };
18910
- }
18911
- 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;
18912
- var init_wait_for_job = __esm({
18913
- "src/mcp/wait-for-job.ts"() {
18914
- "use strict";
18915
- import_node_fs48 = require("fs");
18916
- import_node_path48 = require("path");
18917
- init_workspace_scratch();
18918
- init_wait_for_turn();
18919
- MAX_JOB_CONTINUES = 8;
18920
- MAX_JOB_DELTA_LINES = 80;
18921
- MAX_JOB_DELTA_CHARS = 4e3;
18922
- DEFERRED_DONE_MAX_CHARS = 480;
18923
- GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
18924
- 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.";
18925
- 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.";
18926
- JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
18927
- }
18928
- });
18929
-
18930
18988
  // src/detect/detect.ts
18931
18989
  async function detectAgents() {
18932
18990
  ensureAgentPath();
@@ -23709,6 +23767,13 @@ function isPresentPlanToolName(name) {
23709
23767
  if (!name) return false;
23710
23768
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
23711
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
+ }
23712
23777
  function extractPresentedPlan(parts) {
23713
23778
  if (!parts?.length) return null;
23714
23779
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -23746,7 +23811,7 @@ function resolvePlanMarkdown(opts) {
23746
23811
  }
23747
23812
  return null;
23748
23813
  }
23749
- 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;
23750
23815
  var init_plan_present = __esm({
23751
23816
  "src/plan/plan-present.ts"() {
23752
23817
  "use strict";
@@ -23754,6 +23819,8 @@ var init_plan_present = __esm({
23754
23819
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
23755
23820
  PLAN_FILE_NAME = "plan.md";
23756
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.";
23757
23824
  }
23758
23825
  });
23759
23826
 
@@ -23763,11 +23830,15 @@ __export(plan_file_exports, {
23763
23830
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
23764
23831
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
23765
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,
23766
23835
  extractPresentedPlan: () => extractPresentedPlan,
23767
23836
  isPresentPlanToolName: () => isPresentPlanToolName,
23768
23837
  planFileAbs: () => planFileAbs,
23769
23838
  readPlanFile: () => readPlanFile,
23770
23839
  resolvePlanMarkdown: () => resolvePlanMarkdown,
23840
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
23841
+ threadMayPresentPlan: () => threadMayPresentPlan,
23771
23842
  writePlanFile: () => writePlanFile
23772
23843
  });
23773
23844
  function ensureAttachmentsGitignore(worktreePath) {
@@ -24903,7 +24974,8 @@ var init_orchestrator = __esm({
24903
24974
  if (compact.didCompact) {
24904
24975
  thread = updateThread(threadId, {
24905
24976
  messages: compact.thread.messages,
24906
- sessionId: compact.thread.sessionId
24977
+ sessionId: compact.thread.sessionId,
24978
+ compactedMessages: compact.thread.compactedMessages
24907
24979
  });
24908
24980
  this.emit({
24909
24981
  type: "context_compacted",
@@ -24917,9 +24989,6 @@ var init_orchestrator = __esm({
24917
24989
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
24918
24990
  thread = this.requireThread(threadId);
24919
24991
  }
24920
- if (this.stoppedTurns.has(threadId)) {
24921
- return;
24922
- }
24923
24992
  const promptText = typeof prompt === "string" ? prompt : "";
24924
24993
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
24925
24994
  const parked = takePendingTurnAttachments(thread);
@@ -24936,6 +25005,14 @@ var init_orchestrator = __esm({
24936
25005
  agent: thread.agent,
24937
25006
  orch: isOrchestratorThread(thread)
24938
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
+ }
24939
25016
  thread = this.requireThread(threadId);
24940
25017
  const gitPrompt = expandCanonicalGitRequest(promptText);
24941
25018
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -24994,7 +25071,7 @@ var init_orchestrator = __esm({
24994
25071
  ].filter(Boolean).join("\n\n") || void 0;
24995
25072
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
24996
25073
  const agentPrompt = [
24997
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
25074
+ planModeTurnInstruction(thread.planMode),
24998
25075
  orchestrationReminder,
24999
25076
  prGateDirective,
25000
25077
  injectedNoticeContext,
@@ -26928,8 +27005,11 @@ __export(index_exports, {
26928
27005
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
26929
27006
  PLAN_FILE_REL: () => PLAN_FILE_REL,
26930
27007
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
27008
+ PLAN_MODE_OFF_INSTRUCTION: () => PLAN_MODE_OFF_INSTRUCTION,
26931
27009
  PLAN_QUESTION_ANSWERS_PREFIX: () => PLAN_QUESTION_ANSWERS_PREFIX,
26932
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,
26933
27013
  PROFILE_NOTES_MAX: () => PROFILE_NOTES_MAX,
26934
27014
  PR_DETAILS_JSON_FIELDS: () => PR_DETAILS_JSON_FIELDS,
26935
27015
  REPO_REVIEW_NAME: () => REPO_REVIEW_NAME,
@@ -27000,6 +27080,7 @@ __export(index_exports, {
27000
27080
  annotateDevAccessRuns: () => annotateDevAccessRuns,
27001
27081
  appDataDir: () => appDataDir,
27002
27082
  appSettingsPath: () => appSettingsPath,
27083
+ appendCompactedMessages: () => appendCompactedMessages,
27003
27084
  appendIndexedGitConfig: () => appendIndexedGitConfig,
27004
27085
  appendMessage: () => appendMessage,
27005
27086
  appendQueuedItem: () => appendQueuedItem,
@@ -27087,6 +27168,9 @@ __export(index_exports, {
27087
27168
  commentGitHubIssue: () => commentGitHubIssue,
27088
27169
  commentLinearIssue: () => commentLinearIssue,
27089
27170
  commitAll: () => commitAll,
27171
+ compactArchiveProse: () => compactArchiveProse,
27172
+ compactPartsForSummaryMessage: () => compactPartsForSummaryMessage,
27173
+ compactSummaryPart: () => compactSummaryPart,
27090
27174
  computeNextRunAt: () => computeNextRunAt,
27091
27175
  conductorBundledBinDir: () => conductorBundledBinDir,
27092
27176
  conductorDbPath: () => conductorDbPath,
@@ -27486,6 +27570,7 @@ __export(index_exports, {
27486
27570
  mergeSideboardIntoMcpServersJson: () => mergeSideboardIntoMcpServersJson,
27487
27571
  mergeUsage: () => mergeUsage,
27488
27572
  messagePartParentId: () => messagePartParentId,
27573
+ messagesForCompactArchive: () => messagesForCompactArchive,
27489
27574
  messagesSinceLastBrightsyContextSummary: () => messagesSinceLastBrightsyContextSummary,
27490
27575
  moveQueuedItemToFront: () => moveQueuedItemToFront,
27491
27576
  needsCoordinatorAction: () => needsCoordinatorAction,
@@ -27545,6 +27630,7 @@ __export(index_exports, {
27545
27630
  planFileAbs: () => planFileAbs,
27546
27631
  planHistoryAgePurge: () => planHistoryAgePurge,
27547
27632
  planHistoryRetention: () => planHistoryRetention,
27633
+ planModeTurnInstruction: () => planModeTurnInstruction,
27548
27634
  planQuestionsSignature: () => planQuestionsSignature,
27549
27635
  pnpmFilterPackage: () => pnpmFilterPackage,
27550
27636
  pollSlackOutboundWatches: () => pollSlackOutboundWatches,
@@ -27672,6 +27758,7 @@ __export(index_exports, {
27672
27758
  shouldInjectBrightsyMcp: () => shouldInjectBrightsyMcp,
27673
27759
  shouldNotifyOrchestrator: () => shouldNotifyOrchestrator,
27674
27760
  shouldRefreshReviewRequestTemplate: () => shouldRefreshReviewRequestTemplate,
27761
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
27675
27762
  shouldRemoveWorktreeOnTeardown: () => shouldRemoveWorktreeOnTeardown,
27676
27763
  shouldResetSessionForOccupancy: () => shouldResetSessionForOccupancy,
27677
27764
  shouldRunHistoryCleanup: () => shouldRunHistoryCleanup,
@@ -27733,6 +27820,7 @@ __export(index_exports, {
27733
27820
  threadHasCompactedContext: () => threadHasCompactedContext,
27734
27821
  threadLivePath: () => threadLivePath,
27735
27822
  threadLockPath: () => threadLockPath,
27823
+ threadMayPresentPlan: () => threadMayPresentPlan,
27736
27824
  threadRequestsBrightsyMcp: () => threadRequestsBrightsyMcp,
27737
27825
  threadsDir: () => threadsDir,
27738
27826
  threadsSharingWorktree: () => threadsSharingWorktree,
@@ -27906,7 +27994,8 @@ function slimThreadForUiList(thread) {
27906
27994
  attachments: [],
27907
27995
  pendingTurnAttachments: [],
27908
27996
  queueAttachments: [],
27909
- messages: slimMessagesForUiList(messages, thread.updatedAt)
27997
+ messages: slimMessagesForUiList(messages, thread.updatedAt),
27998
+ compactedMessages: void 0
27910
27999
  };
27911
28000
  }
27912
28001
  function slimMessagesForUiList(messages, fallbackTs) {
@@ -30460,6 +30549,7 @@ function mcpArchiveBlockedReason(thread) {
30460
30549
 
30461
30550
  // src/mcp/server.ts
30462
30551
  init_profile();
30552
+ init_plan_present();
30463
30553
 
30464
30554
  // src/mcp/run-script-ref.ts
30465
30555
  init_chat_tabs();
@@ -33372,31 +33462,40 @@ async function startMcpServer() {
33372
33462
  return mcpJson(payload);
33373
33463
  }
33374
33464
  );
33375
- server.tool(
33376
- "present_plan",
33377
- "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.",
33378
- {
33379
- title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
33380
- content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
33381
- thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
33382
- },
33383
- async ({ title, content, thread_id }) => {
33384
- const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
33385
- let root = process.cwd();
33386
- if (thread_id?.trim()) {
33387
- 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();
33388
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);
33389
33496
  }
33390
- const path2 = writePlanFile2(root, content);
33391
- const payload = {
33392
- ok: true,
33393
- path: path2,
33394
- title: title?.trim() || "Plan",
33395
- message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
33396
- };
33397
- return mcpJson(payload);
33398
- }
33399
- );
33497
+ );
33498
+ }
33400
33499
  server.tool(
33401
33500
  "present_schema",
33402
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.",
@@ -36733,8 +36832,11 @@ async function startSlackRelayServer(opts) {
36733
36832
  PLAN_FILE_NAME,
36734
36833
  PLAN_FILE_REL,
36735
36834
  PLAN_MODE_INSTRUCTION,
36835
+ PLAN_MODE_OFF_INSTRUCTION,
36736
36836
  PLAN_QUESTION_ANSWERS_PREFIX,
36737
36837
  PORT_RANGE_SIZE,
36838
+ PRESENT_PLAN_REQUIRES_PLAN_MODE,
36839
+ PRESENT_PLAN_TOOL_DESCRIPTION,
36738
36840
  PROFILE_NOTES_MAX,
36739
36841
  PR_DETAILS_JSON_FIELDS,
36740
36842
  REPO_REVIEW_NAME,
@@ -36805,6 +36907,7 @@ async function startSlackRelayServer(opts) {
36805
36907
  annotateDevAccessRuns,
36806
36908
  appDataDir,
36807
36909
  appSettingsPath,
36910
+ appendCompactedMessages,
36808
36911
  appendIndexedGitConfig,
36809
36912
  appendMessage,
36810
36913
  appendQueuedItem,
@@ -36892,6 +36995,9 @@ async function startSlackRelayServer(opts) {
36892
36995
  commentGitHubIssue,
36893
36996
  commentLinearIssue,
36894
36997
  commitAll,
36998
+ compactArchiveProse,
36999
+ compactPartsForSummaryMessage,
37000
+ compactSummaryPart,
36895
37001
  computeNextRunAt,
36896
37002
  conductorBundledBinDir,
36897
37003
  conductorDbPath,
@@ -37291,6 +37397,7 @@ async function startSlackRelayServer(opts) {
37291
37397
  mergeSideboardIntoMcpServersJson,
37292
37398
  mergeUsage,
37293
37399
  messagePartParentId,
37400
+ messagesForCompactArchive,
37294
37401
  messagesSinceLastBrightsyContextSummary,
37295
37402
  moveQueuedItemToFront,
37296
37403
  needsCoordinatorAction,
@@ -37350,6 +37457,7 @@ async function startSlackRelayServer(opts) {
37350
37457
  planFileAbs,
37351
37458
  planHistoryAgePurge,
37352
37459
  planHistoryRetention,
37460
+ planModeTurnInstruction,
37353
37461
  planQuestionsSignature,
37354
37462
  pnpmFilterPackage,
37355
37463
  pollSlackOutboundWatches,
@@ -37477,6 +37585,7 @@ async function startSlackRelayServer(opts) {
37477
37585
  shouldInjectBrightsyMcp,
37478
37586
  shouldNotifyOrchestrator,
37479
37587
  shouldRefreshReviewRequestTemplate,
37588
+ shouldRegisterPresentPlan,
37480
37589
  shouldRemoveWorktreeOnTeardown,
37481
37590
  shouldResetSessionForOccupancy,
37482
37591
  shouldRunHistoryCleanup,
@@ -37538,6 +37647,7 @@ async function startSlackRelayServer(opts) {
37538
37647
  threadHasCompactedContext,
37539
37648
  threadLivePath,
37540
37649
  threadLockPath,
37650
+ threadMayPresentPlan,
37541
37651
  threadRequestsBrightsyMcp,
37542
37652
  threadsDir,
37543
37653
  threadsSharingWorktree,