@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
@@ -1660,7 +1660,8 @@ function normalizeThread(raw) {
1660
1660
  runScriptRequest: normalizeRunScriptRequest(raw.runScriptRequest),
1661
1661
  quotaResumeAt: raw.quotaResumeAt ?? null,
1662
1662
  quotaContinuedFromId: raw.quotaContinuedFromId ?? null,
1663
- archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0
1663
+ archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0,
1664
+ compactedMessages: Array.isArray(raw.compactedMessages) ? raw.compactedMessages : void 0
1664
1665
  };
1665
1666
  }
1666
1667
  function createEmptyThread(partial) {
@@ -3400,6 +3401,7 @@ function archivedUpdatedMs(thread) {
3400
3401
  }
3401
3402
  function isHistoryRetentionStub(thread) {
3402
3403
  if ((thread.attachments?.length ?? 0) > 0) return false;
3404
+ if ((thread.compactedMessages?.length ?? 0) > 0) return false;
3403
3405
  const messages = thread.messages ?? [];
3404
3406
  if (messages.length === 0) return true;
3405
3407
  if (messages.length > 1) return false;
@@ -9704,6 +9706,10 @@ var init_node_launch = __esm({
9704
9706
  // src/agents/injected-mcp.ts
9705
9707
  function sideboardWorktreeAllowedTools(opts) {
9706
9708
  const out = [...SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS];
9709
+ if (!opts?.planMode) {
9710
+ const idx = out.indexOf("mcp__sideboard__present_plan");
9711
+ if (idx >= 0) out.splice(idx, 1);
9712
+ }
9707
9713
  if (opts?.github !== false) out.push(...SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS);
9708
9714
  if (opts?.linear) out.push(...SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS);
9709
9715
  if (opts?.abletime) out.push(...SIDEBOARD_ABLETIME_MCP_ALLOWED_TOOLS);
@@ -10564,6 +10570,9 @@ function skillToolFromTextPart(part, id) {
10564
10570
  updatedAt: Date.now()
10565
10571
  };
10566
10572
  }
10573
+ function compactSummaryPart(text7, id = "compact-summary") {
10574
+ return compactToolPart(text7, id, { status: "done" });
10575
+ }
10567
10576
  function compactToolPart(text7, id, opts) {
10568
10577
  const input = { trigger: opts?.trigger ?? "auto" };
10569
10578
  return withPartTimes(
@@ -10841,6 +10850,17 @@ function partsToAssistantText(parts) {
10841
10850
  (p) => p.type === "text" && !messagePartParentId(p)
10842
10851
  ).map((p) => p.text).join("").trim();
10843
10852
  }
10853
+ function lastAssistantMessageText(parts) {
10854
+ if (!parts?.length) return "";
10855
+ for (let i = parts.length - 1; i >= 0; i--) {
10856
+ const p = parts[i];
10857
+ if (!p || p.type !== "text" || messagePartParentId(p)) continue;
10858
+ const text7 = p.text.trim();
10859
+ if (!text7 || isInternalAgentStatusText(text7)) continue;
10860
+ return text7;
10861
+ }
10862
+ return "";
10863
+ }
10844
10864
  function stripBrightsyNdjsonNoise(text7) {
10845
10865
  if (!text7 || !text7.includes('"type"')) return text7;
10846
10866
  let out = text7;
@@ -10878,6 +10898,9 @@ var init_message_parts = __esm({
10878
10898
  });
10879
10899
 
10880
10900
  // src/agents/types.ts
10901
+ function planModeTurnInstruction(planMode) {
10902
+ return planMode ? PLAN_MODE_INSTRUCTION : PLAN_MODE_OFF_INSTRUCTION;
10903
+ }
10881
10904
  function permissionMode(thread) {
10882
10905
  if (thread.sourceType === "orchestration") {
10883
10906
  return {
@@ -10916,11 +10939,12 @@ function permissionMode(thread) {
10916
10939
  codexSandbox: "workspace-write"
10917
10940
  };
10918
10941
  }
10919
- var PLAN_MODE_INSTRUCTION;
10942
+ var PLAN_MODE_INSTRUCTION, PLAN_MODE_OFF_INSTRUCTION;
10920
10943
  var init_types = __esm({
10921
10944
  "src/agents/types.ts"() {
10922
10945
  "use strict";
10923
10946
  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.";
10947
+ 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.";
10924
10948
  }
10925
10949
  });
10926
10950
 
@@ -11433,7 +11457,8 @@ var init_claude = __esm({
11433
11457
  ...sideboardWorktreeAllowedTools({
11434
11458
  github: true,
11435
11459
  linear: isLinearConnected(),
11436
- abletime: isAbleTimeConnected()
11460
+ abletime: isAbleTimeConnected(),
11461
+ planMode: thread.planMode
11437
11462
  }),
11438
11463
  ...brightsyMcpAllowedTools(injectedBrightsyNames)
11439
11464
  ];
@@ -13983,12 +14008,38 @@ ${match.text}`, "");
13983
14008
  blocks.push("Continue from this context. Do not repeat the summary unless asked.");
13984
14009
  return blocks.join("\n");
13985
14010
  }
14011
+ function compactArchiveProse(message) {
14012
+ if (message.origin === "continue") return null;
14013
+ if (message.role === "summary") return null;
14014
+ let text7 = (message.text ?? "").trim();
14015
+ if (!text7 && message.role === "agent") {
14016
+ text7 = lastAssistantMessageText(message.parts).trim();
14017
+ }
14018
+ if (!text7 || isInternalAgentStatusText(text7)) return null;
14019
+ return { role: message.role, text: text7, ts: message.ts };
14020
+ }
14021
+ function messagesForCompactArchive(older) {
14022
+ const out = [];
14023
+ for (const message of older) {
14024
+ const prose = compactArchiveProse(message);
14025
+ if (prose) out.push(prose);
14026
+ }
14027
+ return out;
14028
+ }
14029
+ function appendCompactedMessages(existing, older) {
14030
+ const next = messagesForCompactArchive(older);
14031
+ if (!existing?.length) return next;
14032
+ if (!next.length) return existing;
14033
+ return [...existing, ...next];
14034
+ }
13986
14035
  function applyCompaction(messages, summaryText, thresholds = {}) {
13987
14036
  const { older, recent } = splitForCompaction(messages, thresholds);
13988
14037
  if (older.length === 0) return messages;
14038
+ const text7 = summaryText.trim();
13989
14039
  const summary = {
13990
14040
  role: "summary",
13991
- text: summaryText.trim(),
14041
+ text: text7,
14042
+ parts: [compactSummaryPart(text7)],
13992
14043
  ts: (/* @__PURE__ */ new Date()).toISOString()
13993
14044
  };
13994
14045
  return [summary, ...recent];
@@ -14016,6 +14067,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
14016
14067
  cwd: thread.worktreePath
14017
14068
  });
14018
14069
  let messages = applyCompaction(thread.messages, summary, thresholds);
14070
+ const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
14019
14071
  const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
14020
14072
  if (resetSession) {
14021
14073
  messages = applyForwardOccupancy(messages);
@@ -14023,6 +14075,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
14023
14075
  const next = {
14024
14076
  ...thread,
14025
14077
  messages,
14078
+ ...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
14026
14079
  sessionId: resetSession ? null : thread.sessionId,
14027
14080
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14028
14081
  };
@@ -14038,6 +14091,7 @@ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAG
14038
14091
  var init_context_compact = __esm({
14039
14092
  "src/composer/context-compact.ts"() {
14040
14093
  "use strict";
14094
+ init_message_parts();
14041
14095
  init_error_detail();
14042
14096
  init_usage();
14043
14097
  init_context_estimate();
@@ -14596,390 +14650,797 @@ var init_worktree_run_ports = __esm({
14596
14650
  }
14597
14651
  });
14598
14652
 
14599
- // src/hook/conductor.ts
14600
- function matchSimpleGlob(pattern, name) {
14601
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
14602
- return new RegExp(`^${escaped}$`).test(name);
14603
- }
14604
- function readWorktreeInclude(repoPath) {
14605
- const path = (0, import_node_path39.join)(repoPath, ".worktreeinclude");
14606
- if (!(0, import_node_fs39.existsSync)(path)) return [];
14607
- return (0, import_node_fs39.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
14653
+ // src/mcp/wait-for-turn.ts
14654
+ function mcpWaitForTurnTimeoutMs(requested) {
14655
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
14656
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
14657
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
14608
14658
  }
14609
- function resolveFilesToCopy(repoPath) {
14610
- const fromInclude = readWorktreeInclude(repoPath);
14611
- if (fromInclude.length) return fromInclude;
14612
- const settings = loadRepoSettings(repoPath);
14613
- if (settings?.filesToCopy?.length) return settings.filesToCopy;
14614
- if (settings?.fileIncludeGlobs?.length) {
14615
- const matched = [];
14616
- try {
14617
- for (const entry of (0, import_node_fs39.readdirSync)(repoPath, { withFileTypes: true })) {
14618
- if (!entry.isFile()) continue;
14619
- for (const glob of settings.fileIncludeGlobs) {
14620
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path39.basename)(glob), entry.name)) {
14621
- matched.push(entry.name);
14622
- break;
14623
- }
14624
- }
14625
- }
14626
- } catch {
14627
- }
14628
- if (matched.length) return [...new Set(matched)];
14659
+ function mcpWaitTaskHint(taskState, status) {
14660
+ switch (taskState) {
14661
+ case "submitted":
14662
+ return MCP_WAIT_QUEUED_HINT;
14663
+ case "working":
14664
+ return MCP_WAIT_STILL_RUNNING_HINT;
14665
+ case "input-required":
14666
+ return MCP_WAIT_INPUT_REQUIRED_HINT;
14667
+ case "canceled":
14668
+ return MCP_WAIT_STOPPED_HINT;
14669
+ case "failed":
14670
+ return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
14671
+ default:
14672
+ return void 0;
14629
14673
  }
14630
- const defaults = [];
14631
- try {
14632
- for (const entry of (0, import_node_fs39.readdirSync)(repoPath, { withFileTypes: true })) {
14633
- if (entry.isFile() && entry.name.startsWith(".env")) {
14634
- defaults.push(entry.name);
14635
- }
14636
- }
14637
- } catch {
14674
+ }
14675
+ 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;
14676
+ var init_wait_for_turn = __esm({
14677
+ "src/mcp/wait-for-turn.ts"() {
14678
+ "use strict";
14679
+ MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
14680
+ 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.";
14681
+ 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.";
14682
+ 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.";
14683
+ MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
14684
+ 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.";
14685
+ 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.";
14638
14686
  }
14639
- if (defaults.length) return defaults;
14640
- return [".env.local", ".env"];
14687
+ });
14688
+
14689
+ // src/mcp/wait-for-job.ts
14690
+ function mcpWaitForJobTimeoutMs(requested) {
14691
+ return mcpWaitForTurnTimeoutMs(requested);
14641
14692
  }
14642
- function copyConfiguredFiles(repoPath, worktreePath) {
14643
- const patterns = resolveFilesToCopy(repoPath);
14644
- const copied = [];
14645
- for (const rel of patterns) {
14646
- const src = (0, import_node_path39.join)(repoPath, rel);
14647
- if (!(0, import_node_fs39.existsSync)(src)) continue;
14648
- const dest = (0, import_node_path39.join)(worktreePath, rel);
14649
- (0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(dest), { recursive: true });
14650
- (0, import_node_fs39.copyFileSync)(src, dest);
14651
- copied.push(rel);
14693
+ function sanitizeDetachedJobId(id) {
14694
+ const s = id.trim();
14695
+ if (!JOB_ID_RE.test(s)) {
14696
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
14652
14697
  }
14653
- return copied;
14698
+ return s;
14654
14699
  }
14655
- async function captureLoginEnv() {
14656
- if (cachedLoginEnv) return { ...cachedLoginEnv };
14657
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
14700
+ function jobAlive(pid) {
14701
+ if (!Number.isInteger(pid) || pid <= 0) return false;
14658
14702
  try {
14659
- const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
14660
- reject: false,
14661
- timeout: 5e3
14662
- });
14663
- if (result.exitCode === 0 && result.stdout) {
14664
- const env = { ...process.env };
14665
- for (const entry of result.stdout.split("\0")) {
14666
- if (!entry) continue;
14667
- const eq = entry.indexOf("=");
14668
- if (eq <= 0) continue;
14669
- const key = entry.slice(0, eq);
14670
- const value = entry.slice(eq + 1);
14671
- if (key) env[key] = value;
14672
- }
14673
- cachedLoginEnv = env;
14674
- return { ...env };
14675
- }
14703
+ process.kill(pid, 0);
14704
+ return true;
14676
14705
  } catch {
14706
+ return false;
14677
14707
  }
14678
- cachedLoginEnv = { ...process.env };
14679
- return { ...cachedLoginEnv };
14680
14708
  }
14681
- function buildWorkspaceScriptEnv(opts, baseEnv) {
14682
- const env = stripNestedElectronEnv({
14683
- ...baseEnv ?? process.env
14684
- });
14685
- const name = opts.workspaceName ?? (0, import_node_path39.basename)(opts.worktreePath);
14686
- const ports = opts.ports ?? [];
14687
- const primary = ports[0];
14688
- env.SIDEBOARD_WORKSPACE_NAME = name;
14689
- env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
14690
- env.SIDEBOARD_ROOT_PATH = opts.repoPath;
14691
- env.SIDEBOARD_IS_LOCAL = "1";
14692
- if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
14693
- env.CONDUCTOR_WORKSPACE_NAME = name;
14694
- env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
14695
- env.CONDUCTOR_ROOT_PATH = opts.repoPath;
14696
- env.CONDUCTOR_IS_LOCAL = "1";
14697
- if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
14698
- if (primary != null) {
14699
- env.SIDEBOARD_PORT = String(primary);
14700
- env.CONDUCTOR_PORT = String(primary);
14701
- env.PORT = String(primary);
14702
- for (let i = 1; i < ports.length; i++) {
14703
- const p = ports[i];
14704
- env[`SIDEBOARD_PORT_${i}`] = String(p);
14705
- env[`CONDUCTOR_PORT_${i}`] = String(p);
14706
- }
14709
+ function processGroupAlive(pgid) {
14710
+ if (process.platform === "win32") return false;
14711
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
14712
+ try {
14713
+ process.kill(-pgid, 0);
14714
+ return true;
14715
+ } catch (err) {
14716
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
14707
14717
  }
14708
- applyWorktreePkgCacheEnv(env, opts.worktreePath);
14709
- return env;
14710
14718
  }
14711
- function pipeLines(stream, onLine) {
14712
- if (!stream || !onLine) return;
14713
- const rl = (0, import_node_readline3.createInterface)({ input: stream });
14714
- rl.on("line", onLine);
14719
+ function jobTreeAlive(pid) {
14720
+ if (pid == null) return false;
14721
+ return jobAlive(pid) || processGroupAlive(pid);
14715
14722
  }
14716
- function killableListenerPids(raw, selfPid = process.pid) {
14717
- const pids = [];
14718
- for (const token of raw.trim().split(/\s+/)) {
14719
- const pid = Number(token);
14720
- if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
14721
- pids.push(pid);
14722
- }
14723
- return pids;
14723
+ function readIntFile(file) {
14724
+ if (!(0, import_node_fs39.existsSync)(file)) return null;
14725
+ const n = Number.parseInt((0, import_node_fs39.readFileSync)(file, "utf8").trim(), 10);
14726
+ return Number.isInteger(n) ? n : null;
14724
14727
  }
14725
- function listUnixListenerPids(port, selfPid) {
14726
- try {
14727
- const raw = (0, import_node_child_process4.execFileSync)("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
14728
- encoding: "utf8",
14729
- stdio: ["ignore", "pipe", "ignore"],
14730
- timeout: 2e3
14731
- });
14732
- return killableListenerPids(raw, selfPid);
14733
- } catch {
14734
- return [];
14735
- }
14728
+ function jobDir(root, id, legacy = false) {
14729
+ return (0, import_node_path39.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
14736
14730
  }
14737
- function listenerCwd(pid) {
14738
- if (!Number.isFinite(pid) || pid <= 0) return null;
14739
- try {
14740
- const raw = (0, import_node_child_process4.execFileSync)("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
14741
- encoding: "utf8",
14742
- stdio: ["ignore", "pipe", "ignore"],
14743
- timeout: 2e3
14744
- });
14745
- for (const line of raw.split("\n")) {
14746
- if (line.startsWith("n") && line.length > 1) return line.slice(1);
14747
- }
14748
- } catch {
14749
- }
14750
- return null;
14731
+ function resolveJobDir(root, id) {
14732
+ const modern = jobDir(root, id, false);
14733
+ if ((0, import_node_fs39.existsSync)(modern)) return modern;
14734
+ const legacy = jobDir(root, id, true);
14735
+ if ((0, import_node_fs39.existsSync)(legacy)) return legacy;
14736
+ return modern;
14751
14737
  }
14752
- function realpathOrResolve(path) {
14753
- try {
14754
- return (0, import_node_fs39.realpathSync)(path);
14755
- } catch {
14756
- return (0, import_node_path39.resolve)(path);
14738
+ function listJobIdsIn(root, rel) {
14739
+ const dir = (0, import_node_path39.join)(root, rel);
14740
+ if (!(0, import_node_fs39.existsSync)(dir)) return [];
14741
+ return (0, import_node_fs39.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
14742
+ }
14743
+ function listRunningDetachedJobs(worktreePath) {
14744
+ const root = worktreePath.trim();
14745
+ if (!root) return [];
14746
+ const ids = /* @__PURE__ */ new Set([
14747
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
14748
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
14749
+ ]);
14750
+ const running = [];
14751
+ for (const id of ids) {
14752
+ const dir = resolveJobDir(root, id);
14753
+ const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
14754
+ if (jobTreeAlive(pid)) running.push(id);
14757
14755
  }
14756
+ return running.sort();
14758
14757
  }
14759
- function isOtherWorktreeListenerCwd(cwd, worktreePath) {
14760
- const wt = realpathOrResolve(worktreePath);
14761
- const c = realpathOrResolve(cwd);
14762
- if (c === wt || c.startsWith(wt + import_node_path39.sep)) return false;
14763
- const parent = (0, import_node_path39.dirname)(wt);
14764
- const thisName = (0, import_node_path39.basename)(wt);
14765
- if (!thisName || c === parent) return false;
14766
- if (!c.startsWith(parent + import_node_path39.sep)) return false;
14767
- const first = c.slice(parent.length + 1).split(import_node_path39.sep)[0];
14768
- return Boolean(first) && first !== thisName;
14758
+ function looksLikeDeferredDonePromise(text7) {
14759
+ const t = (text7 ?? "").trim();
14760
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
14761
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
14762
+ if (!unquoted) return false;
14763
+ 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);
14769
14764
  }
14770
- function signalPid(pid, signal) {
14771
- try {
14772
- process.kill(pid, signal);
14773
- } catch {
14774
- }
14765
+ function isJobContinuePrompt(text7) {
14766
+ if (typeof text7 !== "string") return false;
14767
+ const t = text7.trim();
14768
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
14775
14769
  }
14776
- function killListenersOnPorts(ports) {
14777
- const selfPid = process.pid;
14778
- for (const port of ports) {
14779
- if (!Number.isFinite(port) || port <= 0) continue;
14780
- try {
14781
- if (process.platform === "win32") {
14782
- (0, import_node_child_process4.execFileSync)(
14783
- "powershell",
14784
- [
14785
- "-NoProfile",
14786
- "-Command",
14787
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
14788
- ],
14789
- { stdio: "ignore", timeout: 3e3 }
14790
- );
14791
- continue;
14792
- }
14793
- for (const pid of listUnixListenerPids(port, selfPid)) {
14794
- signalPid(pid, "SIGTERM");
14795
- }
14796
- } catch {
14797
- }
14770
+ function collapseGhRunWatchLog(text7) {
14771
+ if (!text7.includes(GH_WATCH_REFRESH)) return text7;
14772
+ const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
14773
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
14774
+ if (watchFrames.length <= 1) return text7;
14775
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
14776
+ const last = watchFrames[watchFrames.length - 1] ?? "";
14777
+ const omitted = watchFrames.length - 1;
14778
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
14779
+ `;
14780
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
14781
+ }
14782
+ function capJobLogDelta(text7) {
14783
+ const collapsed = collapseGhRunWatchLog(text7);
14784
+ const lines = collapsed.split("\n");
14785
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
14786
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
14787
+ if (out.length > MAX_JOB_DELTA_CHARS) {
14788
+ out = `\u2026(truncated ${out.length} chars)
14789
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
14798
14790
  }
14791
+ return out;
14799
14792
  }
14800
- function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
14801
- if (!worktreePath.trim()) {
14802
- killListenersOnPorts(ports);
14803
- return;
14793
+ function formatJobStillRunningContinuePrompt(jobIds) {
14794
+ const ids = jobIds.join(", ");
14795
+ return [
14796
+ `Detached job still running: ${ids}.`,
14797
+ "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
14798
+ "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
14799
+ "Then report the result. Do not tell the user you will let them know later."
14800
+ ].join(" ");
14801
+ }
14802
+ function formatDeferredDoneContinuePrompt() {
14803
+ return [
14804
+ "You ended the turn after promising to report later, but no detached job is running.",
14805
+ "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).",
14806
+ "Do not say you will let the user know later."
14807
+ ].join(" ");
14808
+ }
14809
+ function turnWatchedDetachedJob(parts) {
14810
+ return parts.some((p) => {
14811
+ if (p.type !== "tool") return false;
14812
+ if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
14813
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
14814
+ return /detached-job\.(?:js|cjs)\b/i.test(blob);
14815
+ });
14816
+ }
14817
+ function planJobContinue(opts) {
14818
+ if (opts.isOrchestrator) return { action: "none" };
14819
+ if (opts.agent === "brightsy") return { action: "none" };
14820
+ if (opts.queueLength > 0) return { action: "none" };
14821
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
14822
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
14823
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
14824
+ return {
14825
+ action: "wait",
14826
+ jobIds: opts.runningJobIds,
14827
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
14828
+ };
14804
14829
  }
14805
- if (process.platform === "win32") {
14806
- killListenersOnPorts(ports);
14807
- return;
14830
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
14831
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
14808
14832
  }
14809
- const selfPid = process.pid;
14810
- for (const port of ports) {
14811
- if (!Number.isFinite(port) || port <= 0) continue;
14812
- for (const pid of listUnixListenerPids(port, selfPid)) {
14813
- const cwd = listenerCwd(pid);
14814
- if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
14815
- signalPid(pid, signal);
14816
- }
14833
+ return { action: "none" };
14834
+ }
14835
+ function tailProgress(logFile, maxLines = 12) {
14836
+ if (!(0, import_node_fs39.existsSync)(logFile)) return "(no log yet)";
14837
+ const lines = (0, import_node_fs39.readFileSync)(logFile, "utf8").split("\n");
14838
+ return lines.slice(-maxLines).join("\n");
14839
+ }
14840
+ function lastProgressLine(progress) {
14841
+ const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
14842
+ return last ? last.slice(0, 72) : void 0;
14843
+ }
14844
+ function readLogLines(file) {
14845
+ if (!(0, import_node_fs39.existsSync)(file)) return [];
14846
+ const lines = (0, import_node_fs39.readFileSync)(file, "utf8").split("\n");
14847
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
14848
+ return lines;
14849
+ }
14850
+ function takeDelta(logFile, cursorFile) {
14851
+ const lines = readLogLines(logFile);
14852
+ const cursor = readIntFile(cursorFile) ?? 0;
14853
+ const start = Math.min(Math.max(0, cursor), lines.length);
14854
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
14855
+ }
14856
+ function snapshotJob(dir) {
14857
+ const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
14858
+ const running = jobTreeAlive(pid);
14859
+ return {
14860
+ pid,
14861
+ running,
14862
+ exitCode: readIntFile((0, import_node_path39.join)(dir, "exit")),
14863
+ log: (0, import_node_path39.join)(dir, "log"),
14864
+ cursor: (0, import_node_path39.join)(dir, "present.cursor"),
14865
+ progress: tailProgress((0, import_node_path39.join)(dir, "log"))
14866
+ };
14867
+ }
14868
+ function toResult(id, snap, extra) {
14869
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
14870
+ const ok = !snap.running && snap.exitCode === 0;
14871
+ const stillRunning = snap.running && !ok;
14872
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
14873
+ try {
14874
+ (0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(snap.cursor), { recursive: true });
14875
+ (0, import_node_fs39.writeFileSync)(snap.cursor, `${nextCursor}
14876
+ `);
14877
+ } catch {
14878
+ }
14879
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
14880
+ const progress = extra?.progress ?? snap.progress;
14881
+ return {
14882
+ stillRunning,
14883
+ ok,
14884
+ failed: Boolean(failed && !stillRunning && !ok),
14885
+ status,
14886
+ id,
14887
+ pid: snap.pid,
14888
+ exitCode: snap.exitCode ?? void 0,
14889
+ phase: lastProgressLine(progress),
14890
+ delta,
14891
+ progress,
14892
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
14893
+ };
14894
+ }
14895
+ async function sleepMs(ms) {
14896
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
14897
+ }
14898
+ async function waitForDetachedJob(cwd, id, opts) {
14899
+ const jobId = sanitizeDetachedJobId(id);
14900
+ const root = cwd.trim() || process.cwd();
14901
+ const dir = resolveJobDir(root, jobId);
14902
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
14903
+ const sleep = opts?.sleep ?? sleepMs;
14904
+ if (!(0, import_node_fs39.existsSync)(dir)) {
14905
+ return {
14906
+ stillRunning: false,
14907
+ ok: false,
14908
+ failed: true,
14909
+ status: "failed",
14910
+ id: jobId,
14911
+ delta: "",
14912
+ progress: "No detached job. Start one first.",
14913
+ hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
14914
+ };
14915
+ }
14916
+ const deadline = Date.now() + timeoutMs;
14917
+ let snap = snapshotJob(dir);
14918
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
14919
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
14920
+ return toResult(jobId, snap, {
14921
+ failed: true,
14922
+ progress: "No detached job. Start one first."
14923
+ });
14924
+ }
14925
+ while (Date.now() < deadline) {
14926
+ snap = snapshotJob(dir);
14927
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
14928
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
14929
+ await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
14817
14930
  }
14931
+ snap = snapshotJob(dir);
14932
+ return toResult(jobId, snap);
14818
14933
  }
14819
- function killScriptTree(child, ports = []) {
14820
- const pid = child.pid;
14821
- if (pid) {
14934
+ function killProcessTree(pid, signal) {
14935
+ if (!Number.isInteger(pid) || pid <= 0) return;
14936
+ if (process.platform !== "win32") {
14822
14937
  try {
14823
- if (process.platform === "win32") {
14824
- void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
14825
- } else {
14826
- process.kill(-pid, "SIGTERM");
14827
- setTimeout(() => {
14828
- try {
14829
- process.kill(-pid, "SIGKILL");
14830
- } catch {
14831
- }
14832
- }, 2500).unref?.();
14833
- }
14938
+ process.kill(-pid, signal);
14939
+ return;
14834
14940
  } catch {
14835
- try {
14836
- child.kill("SIGTERM");
14837
- } catch {
14838
- }
14839
14941
  }
14840
14942
  }
14841
- killListenersOnPorts(ports);
14842
- }
14843
- async function spawnWorkspaceScript(command, opts) {
14844
- const loginEnv = await captureLoginEnv();
14845
- const env = buildWorkspaceScriptEnv(
14846
- {
14847
- worktreePath: opts.worktreePath,
14848
- repoPath: opts.repoPath,
14849
- workspaceName: opts.workspaceName,
14850
- defaultBranch: opts.defaultBranch,
14851
- ports: opts.ports
14852
- },
14853
- loginEnv
14854
- );
14855
14943
  try {
14856
- mergeAgentGitAuthEnv(
14857
- env,
14858
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
14859
- );
14944
+ process.kill(pid, signal);
14860
14945
  } catch {
14861
14946
  }
14862
- if (opts.heldPorts?.length) {
14863
- await Promise.all(opts.heldPorts.map((h) => h.release()));
14864
- }
14865
- const shell = process.platform === "darwin" ? "zsh" : "bash";
14866
- const child = (0, import_execa4.execa)(shell, ["-lc", command], {
14867
- cwd: opts.worktreePath,
14868
- reject: false,
14869
- env,
14870
- // Own process group so Stop can tear down the whole tree (not just the shell).
14871
- // There is no settings.toml `stop=` / teardown hook for run scripts.
14872
- ...process.platform === "win32" ? {} : { detached: true }
14873
- });
14874
- pipeLines(child.stdout, opts.onLine);
14875
- pipeLines(child.stderr, opts.onLine);
14876
- const ports = opts.ports ?? [];
14877
- return {
14878
- pid: child.pid,
14879
- kill: () => killScriptTree(child, ports),
14880
- done: child.then((r) => r.exitCode ?? null),
14881
- child
14882
- };
14883
14947
  }
14884
- async function attachAbort(handle, signal) {
14885
- if (signal) {
14886
- const onAbort = () => handle.kill();
14887
- if (signal.aborted) onAbort();
14888
- else signal.addEventListener("abort", onAbort, { once: true });
14948
+ function appendJobLog(logFile, line) {
14949
+ try {
14950
+ (0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(logFile), { recursive: true });
14951
+ const body = line.endsWith("\n") ? line : `${line}
14952
+ `;
14953
+ (0, import_node_fs39.appendFileSync)(logFile, body);
14954
+ } catch {
14889
14955
  }
14890
- return handle.done;
14891
14956
  }
14892
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
14893
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14894
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
14895
- const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
14896
- onLine?.(`[setup] ${source ?? "settings.toml"}`);
14897
- const handle = await spawnWorkspaceScript(settings.setup, {
14898
- worktreePath,
14899
- repoPath,
14900
- defaultBranch: opts?.defaultBranch,
14901
- onLine
14902
- });
14903
- const exitCode = await attachAbort(handle, opts?.signal);
14957
+ async function stopDetachedJob(cwd, id, opts) {
14958
+ const jobId = sanitizeDetachedJobId(id);
14959
+ const root = cwd.trim() || process.cwd();
14960
+ const dir = resolveJobDir(root, jobId);
14961
+ const sleep = opts?.sleep ?? sleepMs;
14962
+ const why = opts?.reason?.trim() ?? "";
14963
+ if (!(0, import_node_fs39.existsSync)(dir)) {
14964
+ return {
14965
+ stillRunning: false,
14966
+ ok: false,
14967
+ failed: true,
14968
+ status: "failed",
14969
+ id: jobId,
14970
+ delta: "",
14971
+ progress: "No detached job. Start one first.",
14972
+ hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
14973
+ stopped: false,
14974
+ reason: "not-found"
14975
+ };
14976
+ }
14977
+ let snap = snapshotJob(dir);
14978
+ if (!snap.running) {
14979
+ const result2 = toResult(jobId, snap);
14980
+ return {
14981
+ ...result2,
14982
+ stopped: false,
14983
+ reason: "not-running",
14984
+ hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
14985
+ };
14986
+ }
14987
+ appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
14988
+ const targetPid = snap.pid;
14989
+ if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
14990
+ const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
14991
+ const deadline = Date.now() + graceMs;
14992
+ while (Date.now() < deadline && jobTreeAlive(targetPid)) {
14993
+ await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
14994
+ }
14995
+ if (targetPid != null) {
14996
+ killProcessTree(targetPid, "SIGKILL");
14997
+ await sleep(100);
14998
+ }
14999
+ const exitFile = (0, import_node_path39.join)(dir, "exit");
15000
+ if (!(0, import_node_fs39.existsSync)(exitFile)) {
15001
+ try {
15002
+ (0, import_node_fs39.writeFileSync)(exitFile, "1\n");
15003
+ } catch {
15004
+ }
15005
+ }
15006
+ snap = snapshotJob(dir);
15007
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
15008
+ const result = toResult(jobId, snap, { failed: true });
15009
+ const stopped = !stillRunning;
14904
15010
  return {
14905
- ran: true,
14906
- exitCode,
14907
- source,
14908
- kill: handle.kill
15011
+ ...result,
15012
+ stillRunning,
15013
+ ok: false,
15014
+ failed: true,
15015
+ status: stillRunning ? "running" : "failed",
15016
+ stopped,
15017
+ reason: stopped ? "stopped" : "still-running",
15018
+ stopReason: why || void 0,
15019
+ hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
14909
15020
  };
14910
15021
  }
14911
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
14912
- const found = findConventionSetup(worktreePath, repoPath);
14913
- if (!found) return { ran: false, exitCode: null, source: null };
14914
- onLine?.(`[setup] ${found.source}`);
14915
- const handle = await spawnWorkspaceScript(found.command, {
14916
- worktreePath,
14917
- repoPath,
14918
- defaultBranch: opts?.defaultBranch,
14919
- onLine
14920
- });
14921
- const exitCode = await attachAbort(handle, opts?.signal);
14922
- return {
14923
- ran: true,
14924
- exitCode,
14925
- source: found.source,
14926
- kill: handle.kill
14927
- };
15022
+ var import_node_fs39, import_node_path39, 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;
15023
+ var init_wait_for_job = __esm({
15024
+ "src/mcp/wait-for-job.ts"() {
15025
+ "use strict";
15026
+ import_node_fs39 = require("fs");
15027
+ import_node_path39 = require("path");
15028
+ init_workspace_scratch();
15029
+ init_wait_for_turn();
15030
+ MAX_JOB_CONTINUES = 8;
15031
+ MAX_JOB_DELTA_LINES = 80;
15032
+ MAX_JOB_DELTA_CHARS = 4e3;
15033
+ DEFERRED_DONE_MAX_CHARS = 480;
15034
+ GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
15035
+ 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.";
15036
+ 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.";
15037
+ JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
15038
+ }
15039
+ });
15040
+
15041
+ // src/hook/conductor.ts
15042
+ function matchSimpleGlob(pattern, name) {
15043
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
15044
+ return new RegExp(`^${escaped}$`).test(name);
14928
15045
  }
14929
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
14930
- await ensureWorktreeSideboardIgnored(worktreePath);
14931
- const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
14932
- if (review.wrote) {
14933
- onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
15046
+ function readWorktreeInclude(repoPath) {
15047
+ const path = (0, import_node_path40.join)(repoPath, ".worktreeinclude");
15048
+ if (!(0, import_node_fs40.existsSync)(path)) return [];
15049
+ return (0, import_node_fs40.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15050
+ }
15051
+ function resolveFilesToCopy(repoPath) {
15052
+ const fromInclude = readWorktreeInclude(repoPath);
15053
+ if (fromInclude.length) return fromInclude;
15054
+ const settings = loadRepoSettings(repoPath);
15055
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
15056
+ if (settings?.fileIncludeGlobs?.length) {
15057
+ const matched = [];
15058
+ try {
15059
+ for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
15060
+ if (!entry.isFile()) continue;
15061
+ for (const glob of settings.fileIncludeGlobs) {
15062
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path40.basename)(glob), entry.name)) {
15063
+ matched.push(entry.name);
15064
+ break;
15065
+ }
15066
+ }
15067
+ }
15068
+ } catch {
15069
+ }
15070
+ if (matched.length) return [...new Set(matched)];
14934
15071
  }
14935
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
14936
- if (!setup.ran) {
14937
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
15072
+ const defaults = [];
15073
+ try {
15074
+ for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
15075
+ if (entry.isFile() && entry.name.startsWith(".env")) {
15076
+ defaults.push(entry.name);
15077
+ }
15078
+ }
15079
+ } catch {
14938
15080
  }
14939
- if (!setup.ran) {
14940
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
15081
+ if (defaults.length) return defaults;
15082
+ return [".env.local", ".env"];
15083
+ }
15084
+ function copyConfiguredFiles(repoPath, worktreePath) {
15085
+ const patterns = resolveFilesToCopy(repoPath);
15086
+ const copied = [];
15087
+ for (const rel of patterns) {
15088
+ const src = (0, import_node_path40.join)(repoPath, rel);
15089
+ if (!(0, import_node_fs40.existsSync)(src)) continue;
15090
+ const dest = (0, import_node_path40.join)(worktreePath, rel);
15091
+ (0, import_node_fs40.mkdirSync)((0, import_node_path40.dirname)(dest), { recursive: true });
15092
+ (0, import_node_fs40.copyFileSync)(src, dest);
15093
+ copied.push(rel);
14941
15094
  }
14942
- return setup;
15095
+ return copied;
14943
15096
  }
14944
- async function runArchiveScript(repoPath, worktreePath, onLine) {
14945
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14946
- if (!settings?.archive) return { ran: false, exitCode: null };
14947
- const handle = await spawnWorkspaceScript(settings.archive, {
14948
- worktreePath,
14949
- repoPath,
14950
- onLine
14951
- });
14952
- const exitCode = await handle.done;
14953
- return { ran: true, exitCode };
15097
+ async function captureLoginEnv() {
15098
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
15099
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
15100
+ try {
15101
+ const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
15102
+ reject: false,
15103
+ timeout: 5e3
15104
+ });
15105
+ if (result.exitCode === 0 && result.stdout) {
15106
+ const env = { ...process.env };
15107
+ for (const entry of result.stdout.split("\0")) {
15108
+ if (!entry) continue;
15109
+ const eq = entry.indexOf("=");
15110
+ if (eq <= 0) continue;
15111
+ const key = entry.slice(0, eq);
15112
+ const value = entry.slice(eq + 1);
15113
+ if (key) env[key] = value;
15114
+ }
15115
+ cachedLoginEnv = env;
15116
+ return { ...env };
15117
+ }
15118
+ } catch {
15119
+ }
15120
+ cachedLoginEnv = { ...process.env };
15121
+ return { ...cachedLoginEnv };
14954
15122
  }
14955
- function listRunScripts(worktreePath, repoPath) {
14956
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14957
- if (!settings?.runScripts.length) return [];
14958
- return settings.runScripts.filter((s) => {
14959
- if (!s.availableIn?.length) return true;
14960
- return s.availableIn.includes("local");
15123
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
15124
+ const env = stripNestedElectronEnv({
15125
+ ...baseEnv ?? process.env
14961
15126
  });
15127
+ const name = opts.workspaceName ?? (0, import_node_path40.basename)(opts.worktreePath);
15128
+ const ports = opts.ports ?? [];
15129
+ const primary = ports[0];
15130
+ env.SIDEBOARD_WORKSPACE_NAME = name;
15131
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
15132
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
15133
+ env.SIDEBOARD_IS_LOCAL = "1";
15134
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
15135
+ env.CONDUCTOR_WORKSPACE_NAME = name;
15136
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
15137
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
15138
+ env.CONDUCTOR_IS_LOCAL = "1";
15139
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
15140
+ if (primary != null) {
15141
+ env.SIDEBOARD_PORT = String(primary);
15142
+ env.CONDUCTOR_PORT = String(primary);
15143
+ env.PORT = String(primary);
15144
+ for (let i = 1; i < ports.length; i++) {
15145
+ const p = ports[i];
15146
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
15147
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
15148
+ }
15149
+ }
15150
+ applyWorktreePkgCacheEnv(env, opts.worktreePath);
15151
+ return env;
14962
15152
  }
14963
- function getDefaultRunScript(worktreePath, repoPath) {
14964
- const scripts = listRunScripts(worktreePath, repoPath);
14965
- if (!scripts.length) return null;
14966
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
15153
+ function pipeLines(stream, onLine) {
15154
+ if (!stream || !onLine) return;
15155
+ const rl = (0, import_node_readline3.createInterface)({ input: stream });
15156
+ rl.on("line", onLine);
14967
15157
  }
14968
- function getRunScript(worktreePath, repoPath, name) {
14969
- const scripts = listRunScripts(worktreePath, repoPath);
14970
- if (!scripts.length) return null;
14971
- if (name) {
14972
- return scripts.find((s) => s.name === name) ?? null;
15158
+ function killableListenerPids(raw, selfPid = process.pid) {
15159
+ const pids = [];
15160
+ for (const token of raw.trim().split(/\s+/)) {
15161
+ const pid = Number(token);
15162
+ if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
15163
+ pids.push(pid);
14973
15164
  }
14974
- return getDefaultRunScript(worktreePath, repoPath);
15165
+ return pids;
14975
15166
  }
14976
- function getRunMode(worktreePath, repoPath) {
14977
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
15167
+ function listUnixListenerPids(port, selfPid) {
15168
+ try {
15169
+ const raw = (0, import_node_child_process4.execFileSync)("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
15170
+ encoding: "utf8",
15171
+ stdio: ["ignore", "pipe", "ignore"],
15172
+ timeout: 2e3
15173
+ });
15174
+ return killableListenerPids(raw, selfPid);
15175
+ } catch {
15176
+ return [];
15177
+ }
14978
15178
  }
14979
- async function reservePort() {
14980
- return new Promise((resolve2, reject) => {
14981
- const server = (0, import_node_net.createServer)();
14982
- let released = false;
15179
+ function listenerCwd(pid) {
15180
+ if (!Number.isFinite(pid) || pid <= 0) return null;
15181
+ try {
15182
+ const raw = (0, import_node_child_process4.execFileSync)("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
15183
+ encoding: "utf8",
15184
+ stdio: ["ignore", "pipe", "ignore"],
15185
+ timeout: 2e3
15186
+ });
15187
+ for (const line of raw.split("\n")) {
15188
+ if (line.startsWith("n") && line.length > 1) return line.slice(1);
15189
+ }
15190
+ } catch {
15191
+ }
15192
+ return null;
15193
+ }
15194
+ function realpathOrResolve(path) {
15195
+ try {
15196
+ return (0, import_node_fs40.realpathSync)(path);
15197
+ } catch {
15198
+ return (0, import_node_path40.resolve)(path);
15199
+ }
15200
+ }
15201
+ function isOtherWorktreeListenerCwd(cwd, worktreePath) {
15202
+ const wt = realpathOrResolve(worktreePath);
15203
+ const c = realpathOrResolve(cwd);
15204
+ if (c === wt || c.startsWith(wt + import_node_path40.sep)) return false;
15205
+ const parent = (0, import_node_path40.dirname)(wt);
15206
+ const thisName = (0, import_node_path40.basename)(wt);
15207
+ if (!thisName || c === parent) return false;
15208
+ if (!c.startsWith(parent + import_node_path40.sep)) return false;
15209
+ const first = c.slice(parent.length + 1).split(import_node_path40.sep)[0];
15210
+ return Boolean(first) && first !== thisName;
15211
+ }
15212
+ function signalPid(pid, signal) {
15213
+ try {
15214
+ process.kill(pid, signal);
15215
+ } catch {
15216
+ }
15217
+ }
15218
+ function isThisWorktreeListenerCwd(cwd, worktreePath) {
15219
+ const wt = realpathOrResolve(worktreePath);
15220
+ const c = realpathOrResolve(cwd);
15221
+ return c === wt || c.startsWith(wt + import_node_path40.sep);
15222
+ }
15223
+ function killDetachedRunTree(pid, worktreePath) {
15224
+ if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
15225
+ const cwd = listenerCwd(pid);
15226
+ if (cwd) {
15227
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
15228
+ } else if (!processGroupAlive(pid)) {
15229
+ return;
15230
+ }
15231
+ try {
15232
+ process.kill(-pid, "SIGTERM");
15233
+ } catch {
15234
+ signalPid(pid, "SIGTERM");
15235
+ }
15236
+ }
15237
+ function killListenersOnPorts(ports) {
15238
+ const selfPid = process.pid;
15239
+ for (const port of ports) {
15240
+ if (!Number.isFinite(port) || port <= 0) continue;
15241
+ try {
15242
+ if (process.platform === "win32") {
15243
+ (0, import_node_child_process4.execFileSync)(
15244
+ "powershell",
15245
+ [
15246
+ "-NoProfile",
15247
+ "-Command",
15248
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
15249
+ ],
15250
+ { stdio: "ignore", timeout: 3e3 }
15251
+ );
15252
+ continue;
15253
+ }
15254
+ for (const pid of listUnixListenerPids(port, selfPid)) {
15255
+ signalPid(pid, "SIGTERM");
15256
+ }
15257
+ } catch {
15258
+ }
15259
+ }
15260
+ }
15261
+ function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
15262
+ if (!worktreePath.trim()) {
15263
+ killListenersOnPorts(ports);
15264
+ return;
15265
+ }
15266
+ if (process.platform === "win32") {
15267
+ killListenersOnPorts(ports);
15268
+ return;
15269
+ }
15270
+ const selfPid = process.pid;
15271
+ for (const port of ports) {
15272
+ if (!Number.isFinite(port) || port <= 0) continue;
15273
+ for (const pid of listUnixListenerPids(port, selfPid)) {
15274
+ const cwd = listenerCwd(pid);
15275
+ if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
15276
+ signalPid(pid, signal);
15277
+ }
15278
+ }
15279
+ }
15280
+ function killScriptTree(child, ports = []) {
15281
+ const pid = child.pid;
15282
+ if (pid) {
15283
+ try {
15284
+ if (process.platform === "win32") {
15285
+ void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
15286
+ } else {
15287
+ process.kill(-pid, "SIGTERM");
15288
+ setTimeout(() => {
15289
+ try {
15290
+ process.kill(-pid, "SIGKILL");
15291
+ } catch {
15292
+ }
15293
+ }, 2500).unref?.();
15294
+ }
15295
+ } catch {
15296
+ try {
15297
+ child.kill("SIGTERM");
15298
+ } catch {
15299
+ }
15300
+ }
15301
+ }
15302
+ killListenersOnPorts(ports);
15303
+ }
15304
+ async function spawnWorkspaceScript(command, opts) {
15305
+ const loginEnv = await captureLoginEnv();
15306
+ const env = buildWorkspaceScriptEnv(
15307
+ {
15308
+ worktreePath: opts.worktreePath,
15309
+ repoPath: opts.repoPath,
15310
+ workspaceName: opts.workspaceName,
15311
+ defaultBranch: opts.defaultBranch,
15312
+ ports: opts.ports
15313
+ },
15314
+ loginEnv
15315
+ );
15316
+ try {
15317
+ mergeAgentGitAuthEnv(
15318
+ env,
15319
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
15320
+ );
15321
+ } catch {
15322
+ }
15323
+ if (opts.heldPorts?.length) {
15324
+ await Promise.all(opts.heldPorts.map((h) => h.release()));
15325
+ }
15326
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
15327
+ const child = (0, import_execa4.execa)(shell, ["-lc", command], {
15328
+ cwd: opts.worktreePath,
15329
+ reject: false,
15330
+ env,
15331
+ // Own process group so Stop can tear down the whole tree (not just the shell).
15332
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
15333
+ ...process.platform === "win32" ? {} : { detached: true }
15334
+ });
15335
+ pipeLines(child.stdout, opts.onLine);
15336
+ pipeLines(child.stderr, opts.onLine);
15337
+ const ports = opts.ports ?? [];
15338
+ return {
15339
+ pid: child.pid,
15340
+ kill: () => killScriptTree(child, ports),
15341
+ done: child.then((r) => r.exitCode ?? null),
15342
+ child
15343
+ };
15344
+ }
15345
+ async function attachAbort(handle, signal) {
15346
+ if (signal) {
15347
+ const onAbort = () => handle.kill();
15348
+ if (signal.aborted) onAbort();
15349
+ else signal.addEventListener("abort", onAbort, { once: true });
15350
+ }
15351
+ return handle.done;
15352
+ }
15353
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
15354
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15355
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
15356
+ const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
15357
+ onLine?.(`[setup] ${source ?? "settings.toml"}`);
15358
+ const handle = await spawnWorkspaceScript(settings.setup, {
15359
+ worktreePath,
15360
+ repoPath,
15361
+ defaultBranch: opts?.defaultBranch,
15362
+ onLine
15363
+ });
15364
+ const exitCode = await attachAbort(handle, opts?.signal);
15365
+ return {
15366
+ ran: true,
15367
+ exitCode,
15368
+ source,
15369
+ kill: handle.kill
15370
+ };
15371
+ }
15372
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
15373
+ const found = findConventionSetup(worktreePath, repoPath);
15374
+ if (!found) return { ran: false, exitCode: null, source: null };
15375
+ onLine?.(`[setup] ${found.source}`);
15376
+ const handle = await spawnWorkspaceScript(found.command, {
15377
+ worktreePath,
15378
+ repoPath,
15379
+ defaultBranch: opts?.defaultBranch,
15380
+ onLine
15381
+ });
15382
+ const exitCode = await attachAbort(handle, opts?.signal);
15383
+ return {
15384
+ ran: true,
15385
+ exitCode,
15386
+ source: found.source,
15387
+ kill: handle.kill
15388
+ };
15389
+ }
15390
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
15391
+ await ensureWorktreeSideboardIgnored(worktreePath);
15392
+ const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
15393
+ if (review.wrote) {
15394
+ onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
15395
+ }
15396
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
15397
+ if (!setup.ran) {
15398
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
15399
+ }
15400
+ if (!setup.ran) {
15401
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
15402
+ }
15403
+ return setup;
15404
+ }
15405
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
15406
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15407
+ if (!settings?.archive) return { ran: false, exitCode: null };
15408
+ const handle = await spawnWorkspaceScript(settings.archive, {
15409
+ worktreePath,
15410
+ repoPath,
15411
+ onLine
15412
+ });
15413
+ const exitCode = await handle.done;
15414
+ return { ran: true, exitCode };
15415
+ }
15416
+ function listRunScripts(worktreePath, repoPath) {
15417
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15418
+ if (!settings?.runScripts.length) return [];
15419
+ return settings.runScripts.filter((s) => {
15420
+ if (!s.availableIn?.length) return true;
15421
+ return s.availableIn.includes("local");
15422
+ });
15423
+ }
15424
+ function getDefaultRunScript(worktreePath, repoPath) {
15425
+ const scripts = listRunScripts(worktreePath, repoPath);
15426
+ if (!scripts.length) return null;
15427
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
15428
+ }
15429
+ function getRunScript(worktreePath, repoPath, name) {
15430
+ const scripts = listRunScripts(worktreePath, repoPath);
15431
+ if (!scripts.length) return null;
15432
+ if (name) {
15433
+ return scripts.find((s) => s.name === name) ?? null;
15434
+ }
15435
+ return getDefaultRunScript(worktreePath, repoPath);
15436
+ }
15437
+ function getRunMode(worktreePath, repoPath) {
15438
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
15439
+ }
15440
+ async function reservePort() {
15441
+ return new Promise((resolve2, reject) => {
15442
+ const server = (0, import_node_net.createServer)();
15443
+ let released = false;
14983
15444
  const release = () => new Promise((res) => {
14984
15445
  if (released) {
14985
15446
  res();
@@ -15140,14 +15601,14 @@ async function startDevServer(repoPath, worktreePath, onLine, opts) {
15140
15601
  throw err;
15141
15602
  }
15142
15603
  }
15143
- var import_node_fs39, import_node_child_process4, import_node_net, import_node_path39, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
15604
+ var import_node_fs40, import_node_child_process4, import_node_net, import_node_path40, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
15144
15605
  var init_conductor = __esm({
15145
15606
  "src/hook/conductor.ts"() {
15146
15607
  "use strict";
15147
- import_node_fs39 = require("fs");
15608
+ import_node_fs40 = require("fs");
15148
15609
  import_node_child_process4 = require("child_process");
15149
15610
  import_node_net = require("net");
15150
- import_node_path39 = require("path");
15611
+ import_node_path40 = require("path");
15151
15612
  import_execa4 = require("execa");
15152
15613
  import_node_readline3 = require("readline");
15153
15614
  init_settings();
@@ -15159,6 +15620,7 @@ var init_conductor = __esm({
15159
15620
  init_request_review();
15160
15621
  init_worktree_exclude();
15161
15622
  init_worktree_run_ports();
15623
+ init_wait_for_job();
15162
15624
  init_worktree_run_ports();
15163
15625
  init_nested_electron_env();
15164
15626
  PORT_RANGE_SIZE = 10;
@@ -15201,7 +15663,7 @@ function materialize(mem) {
15201
15663
  };
15202
15664
  }
15203
15665
  function setupLogPath(threadId) {
15204
- return (0, import_node_path40.join)(threadsDir(), `${threadId}.setup.log.json`);
15666
+ return (0, import_node_path41.join)(threadsDir(), `${threadId}.setup.log.json`);
15205
15667
  }
15206
15668
  function setupLogKeyForWorktree(worktreePath) {
15207
15669
  const norm = normalizeWorktreePath(worktreePath);
@@ -15219,7 +15681,7 @@ function persistNow(threadId) {
15219
15681
  const mem = memory.get(threadId);
15220
15682
  if (!mem) return;
15221
15683
  try {
15222
- (0, import_node_fs40.writeFileSync)(
15684
+ (0, import_node_fs41.writeFileSync)(
15223
15685
  setupLogPath(threadId),
15224
15686
  `${JSON.stringify(materialize(mem))}
15225
15687
  `,
@@ -15240,9 +15702,9 @@ function schedulePersist(threadId) {
15240
15702
  }
15241
15703
  function loadFromDisk(threadId) {
15242
15704
  const path = setupLogPath(threadId);
15243
- if (!(0, import_node_fs40.existsSync)(path)) return null;
15705
+ if (!(0, import_node_fs41.existsSync)(path)) return null;
15244
15706
  try {
15245
- const raw = JSON.parse((0, import_node_fs40.readFileSync)(path, "utf8"));
15707
+ const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
15246
15708
  return {
15247
15709
  output: typeof raw.output === "string" ? raw.output : "",
15248
15710
  running: Boolean(raw.running),
@@ -15298,13 +15760,13 @@ function finishSetupLog(threadId, exitCode, source) {
15298
15760
  persistNow(threadId);
15299
15761
  return materialize(mem);
15300
15762
  }
15301
- var import_node_crypto8, import_node_fs40, import_node_path40, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
15763
+ var import_node_crypto8, import_node_fs41, import_node_path41, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
15302
15764
  var init_setup_log = __esm({
15303
15765
  "src/store/setup-log.ts"() {
15304
15766
  "use strict";
15305
15767
  import_node_crypto8 = require("crypto");
15306
- import_node_fs40 = require("fs");
15307
- import_node_path40 = require("path");
15768
+ import_node_fs41 = require("fs");
15769
+ import_node_path41 = require("path");
15308
15770
  init_worktree_labels();
15309
15771
  init_paths();
15310
15772
  EMPTY = {
@@ -15349,7 +15811,7 @@ function safeScriptToken(scriptName) {
15349
15811
  return token || "dev";
15350
15812
  }
15351
15813
  function runLogPath(key) {
15352
- return (0, import_node_path41.join)(threadsDir(), `${key}.run.log.json`);
15814
+ return (0, import_node_path42.join)(threadsDir(), `${key}.run.log.json`);
15353
15815
  }
15354
15816
  function runLogKeyForWorktree(worktreePath, scriptName) {
15355
15817
  const norm = normalizeWorktreePath(worktreePath);
@@ -15377,7 +15839,7 @@ function persistNow2(key) {
15377
15839
  const mem = memory2.get(key);
15378
15840
  if (!mem) return;
15379
15841
  try {
15380
- (0, import_node_fs41.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15842
+ (0, import_node_fs42.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15381
15843
  `, "utf8");
15382
15844
  } catch {
15383
15845
  }
@@ -15394,9 +15856,9 @@ function schedulePersist2(key) {
15394
15856
  }
15395
15857
  function loadFromDisk2(key) {
15396
15858
  const path = runLogPath(key);
15397
- if (!(0, import_node_fs41.existsSync)(path)) return null;
15859
+ if (!(0, import_node_fs42.existsSync)(path)) return null;
15398
15860
  try {
15399
- const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
15861
+ const raw = JSON.parse((0, import_node_fs42.readFileSync)(path, "utf8"));
15400
15862
  return {
15401
15863
  output: typeof raw.output === "string" ? raw.output : "",
15402
15864
  running: Boolean(raw.running),
@@ -15454,13 +15916,13 @@ function finishRunLog(key, exitCode) {
15454
15916
  function flushRunLog(key) {
15455
15917
  persistNow2(key);
15456
15918
  }
15457
- var import_node_crypto9, import_node_fs41, import_node_path41, EMPTY2, MAX_RUN_LOG_CHARS, DEFAULT_RUN_LOG_TAIL_CHARS, memory2, persistTimers2, PERSIST_DEBOUNCE_MS2;
15919
+ var import_node_crypto9, import_node_fs42, import_node_path42, EMPTY2, MAX_RUN_LOG_CHARS, DEFAULT_RUN_LOG_TAIL_CHARS, memory2, persistTimers2, PERSIST_DEBOUNCE_MS2;
15458
15920
  var init_run_log = __esm({
15459
15921
  "src/store/run-log.ts"() {
15460
15922
  "use strict";
15461
15923
  import_node_crypto9 = require("crypto");
15462
- import_node_fs41 = require("fs");
15463
- import_node_path41 = require("path");
15924
+ import_node_fs42 = require("fs");
15925
+ import_node_path42 = require("path");
15464
15926
  init_worktree_labels();
15465
15927
  init_setup_log();
15466
15928
  init_paths();
@@ -15489,9 +15951,9 @@ async function findOrphanWorktrees(repoPaths) {
15489
15951
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
15490
15952
  );
15491
15953
  const homeRoot = sideboardWorkspacesDir();
15492
- if ((0, import_node_fs42.existsSync)(homeRoot)) {
15954
+ if ((0, import_node_fs43.existsSync)(homeRoot)) {
15493
15955
  try {
15494
- for (const entry of (0, import_node_fs42.readdirSync)(homeRoot, { withFileTypes: true })) {
15956
+ for (const entry of (0, import_node_fs43.readdirSync)(homeRoot, { withFileTypes: true })) {
15495
15957
  if (!entry.isDirectory()) continue;
15496
15958
  void entry;
15497
15959
  }
@@ -15501,7 +15963,7 @@ async function findOrphanWorktrees(repoPaths) {
15501
15963
  const orphans = [];
15502
15964
  const seen = /* @__PURE__ */ new Set();
15503
15965
  for (const repoPath of repos) {
15504
- if (!repoPath || !(0, import_node_fs42.existsSync)(repoPath)) continue;
15966
+ if (!repoPath || !(0, import_node_fs43.existsSync)(repoPath)) continue;
15505
15967
  try {
15506
15968
  const wts = await listWorktrees(repoPath);
15507
15969
  for (const wt of wts) {
@@ -15512,7 +15974,7 @@ async function findOrphanWorktrees(repoPaths) {
15512
15974
  seen.add(path);
15513
15975
  let mtimeMs = 0;
15514
15976
  try {
15515
- mtimeMs = (0, import_node_fs42.statSync)(path).mtimeMs;
15977
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
15516
15978
  } catch {
15517
15979
  mtimeMs = 0;
15518
15980
  }
@@ -15522,16 +15984,16 @@ async function findOrphanWorktrees(repoPaths) {
15522
15984
  }
15523
15985
  try {
15524
15986
  const root = worktreesRoot(repoPath);
15525
- if ((0, import_node_fs42.existsSync)(root)) {
15526
- for (const entry of (0, import_node_fs42.readdirSync)(root, { withFileTypes: true })) {
15987
+ if ((0, import_node_fs43.existsSync)(root)) {
15988
+ for (const entry of (0, import_node_fs43.readdirSync)(root, { withFileTypes: true })) {
15527
15989
  if (!entry.isDirectory()) continue;
15528
- const path = (0, import_node_path42.join)(root, entry.name).replace(/\/$/, "");
15990
+ const path = (0, import_node_path43.join)(root, entry.name).replace(/\/$/, "");
15529
15991
  if (known.has(path) || seen.has(path)) continue;
15530
- if (!(0, import_node_fs42.existsSync)((0, import_node_path42.join)(path, ".git"))) continue;
15992
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path43.join)(path, ".git"))) continue;
15531
15993
  seen.add(path);
15532
15994
  let mtimeMs = 0;
15533
15995
  try {
15534
- mtimeMs = (0, import_node_fs42.statSync)(path).mtimeMs;
15996
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
15535
15997
  } catch {
15536
15998
  mtimeMs = Date.now();
15537
15999
  }
@@ -15583,12 +16045,12 @@ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
15583
16045
  const elapsed = Date.now() - Date.parse(last);
15584
16046
  return elapsed >= intervalHours * 36e5;
15585
16047
  }
15586
- var import_node_fs42, import_node_path42;
16048
+ var import_node_fs43, import_node_path43;
15587
16049
  var init_orphan_cleanup = __esm({
15588
16050
  "src/git/orphan-cleanup.ts"() {
15589
16051
  "use strict";
15590
- import_node_fs42 = require("fs");
15591
- import_node_path42 = require("path");
16052
+ import_node_fs43 = require("fs");
16053
+ import_node_path43 = require("path");
15592
16054
  init_worktree();
15593
16055
  init_thread_store();
15594
16056
  init_paths();
@@ -15604,6 +16066,7 @@ function stripArchivedTranscript(thread) {
15604
16066
  attachments: [],
15605
16067
  pendingTurnAttachments: [],
15606
16068
  queueAttachments: [],
16069
+ compactedMessages: void 0,
15607
16070
  messages: [historyRetentionStubMessage(thread)]
15608
16071
  },
15609
16072
  { touch: false }
@@ -15755,534 +16218,146 @@ var init_apply_into_main = __esm({
15755
16218
  import_execa5 = require("execa");
15756
16219
  init_worktree();
15757
16220
  }
15758
- });
15759
-
15760
- // src/git/clone-repo.ts
15761
- async function cloneRepoIntoSideboard(opts) {
15762
- const url = opts.url.trim();
15763
- if (!url) throw new Error("Clone URL is required");
15764
- let name = opts.name?.trim();
15765
- if (!name) {
15766
- const leaf = (0, import_node_path43.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
15767
- name = leaf || "repo";
15768
- }
15769
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
15770
- const dest = (0, import_node_path43.join)(sideboardReposDir(), name);
15771
- if ((0, import_node_fs43.existsSync)(dest)) {
15772
- const repoPath2 = await resolveRepoRoot(dest);
15773
- const workspace2 = await ensureWorkspace(repoPath2);
15774
- return { repoPath: repoPath2, workspace: workspace2 };
15775
- }
15776
- const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
15777
- if (result.exitCode !== 0) {
15778
- throw new Error(
15779
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
15780
- );
15781
- }
15782
- const repoPath = await resolveRepoRoot(dest);
15783
- const workspace = await ensureWorkspace(repoPath);
15784
- return { repoPath, workspace };
15785
- }
15786
- var import_node_fs43, import_node_path43, import_execa6;
15787
- var init_clone_repo = __esm({
15788
- "src/git/clone-repo.ts"() {
15789
- "use strict";
15790
- import_node_fs43 = require("fs");
15791
- import_node_path43 = require("path");
15792
- import_execa6 = require("execa");
15793
- init_paths();
15794
- init_workspaces();
15795
- init_worktree();
15796
- }
15797
- });
15798
-
15799
- // src/types/thread.ts
15800
- function isAdoptableRunScriptRequest(req) {
15801
- return Boolean(req && !req.fulfilledAt && !req.error);
15802
- }
15803
- var init_thread = __esm({
15804
- "src/types/thread.ts"() {
15805
- "use strict";
15806
- }
15807
- });
15808
-
15809
- // src/store/desktop-host.ts
15810
- function desktopHostPidPath() {
15811
- return (0, import_node_path44.join)(appDataDir(), "desktop-host.pid");
15812
- }
15813
- function readDesktopHostPid() {
15814
- try {
15815
- const pid = Number.parseInt((0, import_node_fs44.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
15816
- if (!Number.isFinite(pid) || pid <= 0) return null;
15817
- return pid;
15818
- } catch {
15819
- return null;
15820
- }
15821
- }
15822
- function pidAlive(pid) {
15823
- try {
15824
- process.kill(pid, 0);
15825
- return true;
15826
- } catch {
15827
- return false;
15828
- }
15829
- }
15830
- function isDesktopHostAlive() {
15831
- const pid = readDesktopHostPid();
15832
- return pid != null && pidAlive(pid);
15833
- }
15834
- function isThisProcessDesktopHost() {
15835
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
15836
- }
15837
- function thisProcessShouldDrainAgentQueues() {
15838
- if (isThisProcessDesktopHost()) return true;
15839
- return !isDesktopHostAlive();
15840
- }
15841
- var import_node_fs44, import_node_path44;
15842
- var init_desktop_host = __esm({
15843
- "src/store/desktop-host.ts"() {
15844
- "use strict";
15845
- import_node_fs44 = require("fs");
15846
- import_node_path44 = require("path");
15847
- init_paths();
15848
- }
15849
- });
15850
-
15851
- // src/orchestrator/child-halt.ts
15852
- function isIncompleteChildStatus(status) {
15853
- return HALT_STATUSES.has(status);
15854
- }
15855
- function childHaltNotice(child, status) {
15856
- const title = child.title?.trim() || "Untitled";
15857
- const link = `[${title}](sideboard://thread/${child.id})`;
15858
- const why = child.lastError?.trim();
15859
- const extra = why ? ` lastError: ${why}` : "";
15860
- return [
15861
- `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
15862
- "This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
15863
- ].join("\n");
15864
- }
15865
- function shouldNotifyParentAfterTurnError(opts) {
15866
- return !opts.quotaFailoverHandled && !opts.crashContinued;
15867
- }
15868
- function shouldNotifyParentOfChildHalt(opts) {
15869
- if (!isIncompleteChildStatus(opts.status)) return false;
15870
- if (!opts.child.parentThreadId) return false;
15871
- if (!opts.parent || opts.parent.status === "archived") return false;
15872
- if (opts.parent.id === opts.child.id) return false;
15873
- return isOrchestratorThread(opts.parent);
15874
- }
15875
- function noticeKey(childId, status) {
15876
- return `${childId}:${status}`;
15877
- }
15878
- function notifyParentOfChildHalt(child, status, send) {
15879
- const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
15880
- if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
15881
- const key = noticeKey(child.id, status);
15882
- if (notified.has(key)) return false;
15883
- notified.add(key);
15884
- const parentId = parent.id;
15885
- void send(parentId, childHaltNotice(child, status)).catch(() => {
15886
- notified.delete(key);
15887
- });
15888
- return true;
15889
- }
15890
- var HALT_STATUSES, notified;
15891
- var init_child_halt = __esm({
15892
- "src/orchestrator/child-halt.ts"() {
15893
- "use strict";
15894
- init_global_workspace();
15895
- init_thread_store();
15896
- HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
15897
- notified = /* @__PURE__ */ new Set();
15898
- }
15899
- });
15900
-
15901
- // src/mcp/wait-for-turn.ts
15902
- function mcpWaitForTurnTimeoutMs(requested) {
15903
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
15904
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
15905
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
15906
- }
15907
- function mcpWaitTaskHint(taskState, status) {
15908
- switch (taskState) {
15909
- case "submitted":
15910
- return MCP_WAIT_QUEUED_HINT;
15911
- case "working":
15912
- return MCP_WAIT_STILL_RUNNING_HINT;
15913
- case "input-required":
15914
- return MCP_WAIT_INPUT_REQUIRED_HINT;
15915
- case "canceled":
15916
- return MCP_WAIT_STOPPED_HINT;
15917
- case "failed":
15918
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
15919
- default:
15920
- return void 0;
15921
- }
15922
- }
15923
- 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;
15924
- var init_wait_for_turn = __esm({
15925
- "src/mcp/wait-for-turn.ts"() {
15926
- "use strict";
15927
- MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
15928
- 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.";
15929
- 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.";
15930
- 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.";
15931
- MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
15932
- 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.";
15933
- 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.";
15934
- }
15935
- });
15936
-
15937
- // src/mcp/wait-for-job.ts
15938
- function mcpWaitForJobTimeoutMs(requested) {
15939
- return mcpWaitForTurnTimeoutMs(requested);
15940
- }
15941
- function sanitizeDetachedJobId(id) {
15942
- const s = id.trim();
15943
- if (!JOB_ID_RE.test(s)) {
15944
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
15945
- }
15946
- return s;
15947
- }
15948
- function jobAlive(pid) {
15949
- if (!Number.isInteger(pid) || pid <= 0) return false;
15950
- try {
15951
- process.kill(pid, 0);
15952
- return true;
15953
- } catch {
15954
- return false;
15955
- }
15956
- }
15957
- function processGroupAlive(pgid) {
15958
- if (process.platform === "win32") return false;
15959
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
15960
- try {
15961
- process.kill(-pgid, 0);
15962
- return true;
15963
- } catch (err) {
15964
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
15965
- }
15966
- }
15967
- function jobTreeAlive(pid) {
15968
- if (pid == null) return false;
15969
- return jobAlive(pid) || processGroupAlive(pid);
15970
- }
15971
- function readIntFile(file) {
15972
- if (!(0, import_node_fs45.existsSync)(file)) return null;
15973
- const n = Number.parseInt((0, import_node_fs45.readFileSync)(file, "utf8").trim(), 10);
15974
- return Number.isInteger(n) ? n : null;
15975
- }
15976
- function jobDir(root, id, legacy = false) {
15977
- return (0, import_node_path45.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
15978
- }
15979
- function resolveJobDir(root, id) {
15980
- const modern = jobDir(root, id, false);
15981
- if ((0, import_node_fs45.existsSync)(modern)) return modern;
15982
- const legacy = jobDir(root, id, true);
15983
- if ((0, import_node_fs45.existsSync)(legacy)) return legacy;
15984
- return modern;
15985
- }
15986
- function listJobIdsIn(root, rel) {
15987
- const dir = (0, import_node_path45.join)(root, rel);
15988
- if (!(0, import_node_fs45.existsSync)(dir)) return [];
15989
- return (0, import_node_fs45.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
15990
- }
15991
- function listRunningDetachedJobs(worktreePath) {
15992
- const root = worktreePath.trim();
15993
- if (!root) return [];
15994
- const ids = /* @__PURE__ */ new Set([
15995
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
15996
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
15997
- ]);
15998
- const running = [];
15999
- for (const id of ids) {
16000
- const dir = resolveJobDir(root, id);
16001
- const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
16002
- if (jobTreeAlive(pid)) running.push(id);
16003
- }
16004
- return running.sort();
16005
- }
16006
- function looksLikeDeferredDonePromise(text7) {
16007
- const t = (text7 ?? "").trim();
16008
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
16009
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
16010
- if (!unquoted) return false;
16011
- 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);
16012
- }
16013
- function isJobContinuePrompt(text7) {
16014
- if (typeof text7 !== "string") return false;
16015
- const t = text7.trim();
16016
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
16017
- }
16018
- function collapseGhRunWatchLog(text7) {
16019
- if (!text7.includes(GH_WATCH_REFRESH)) return text7;
16020
- const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
16021
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
16022
- if (watchFrames.length <= 1) return text7;
16023
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
16024
- const last = watchFrames[watchFrames.length - 1] ?? "";
16025
- const omitted = watchFrames.length - 1;
16026
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
16027
- `;
16028
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
16029
- }
16030
- function capJobLogDelta(text7) {
16031
- const collapsed = collapseGhRunWatchLog(text7);
16032
- const lines = collapsed.split("\n");
16033
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
16034
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
16035
- if (out.length > MAX_JOB_DELTA_CHARS) {
16036
- out = `\u2026(truncated ${out.length} chars)
16037
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
16038
- }
16039
- return out;
16040
- }
16041
- function formatJobStillRunningContinuePrompt(jobIds) {
16042
- const ids = jobIds.join(", ");
16043
- return [
16044
- `Detached job still running: ${ids}.`,
16045
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
16046
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
16047
- "Then report the result. Do not tell the user you will let them know later."
16048
- ].join(" ");
16049
- }
16050
- function formatDeferredDoneContinuePrompt() {
16051
- return [
16052
- "You ended the turn after promising to report later, but no detached job is running.",
16053
- "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).",
16054
- "Do not say you will let the user know later."
16055
- ].join(" ");
16056
- }
16057
- function turnWatchedDetachedJob(parts) {
16058
- return parts.some((p) => {
16059
- if (p.type !== "tool") return false;
16060
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
16061
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
16062
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
16063
- });
16064
- }
16065
- function planJobContinue(opts) {
16066
- if (opts.isOrchestrator) return { action: "none" };
16067
- if (opts.agent === "brightsy") return { action: "none" };
16068
- if (opts.queueLength > 0) return { action: "none" };
16069
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
16070
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
16071
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
16072
- return {
16073
- action: "wait",
16074
- jobIds: opts.runningJobIds,
16075
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
16076
- };
16077
- }
16078
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
16079
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
16080
- }
16081
- return { action: "none" };
16082
- }
16083
- function tailProgress(logFile, maxLines = 12) {
16084
- if (!(0, import_node_fs45.existsSync)(logFile)) return "(no log yet)";
16085
- const lines = (0, import_node_fs45.readFileSync)(logFile, "utf8").split("\n");
16086
- return lines.slice(-maxLines).join("\n");
16087
- }
16088
- function lastProgressLine(progress) {
16089
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
16090
- return last ? last.slice(0, 72) : void 0;
16091
- }
16092
- function readLogLines(file) {
16093
- if (!(0, import_node_fs45.existsSync)(file)) return [];
16094
- const lines = (0, import_node_fs45.readFileSync)(file, "utf8").split("\n");
16095
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
16096
- return lines;
16097
- }
16098
- function takeDelta(logFile, cursorFile) {
16099
- const lines = readLogLines(logFile);
16100
- const cursor = readIntFile(cursorFile) ?? 0;
16101
- const start = Math.min(Math.max(0, cursor), lines.length);
16102
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
16103
- }
16104
- function snapshotJob(dir) {
16105
- const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
16106
- const running = jobTreeAlive(pid);
16107
- return {
16108
- pid,
16109
- running,
16110
- exitCode: readIntFile((0, import_node_path45.join)(dir, "exit")),
16111
- log: (0, import_node_path45.join)(dir, "log"),
16112
- cursor: (0, import_node_path45.join)(dir, "present.cursor"),
16113
- progress: tailProgress((0, import_node_path45.join)(dir, "log"))
16114
- };
16115
- }
16116
- function toResult(id, snap, extra) {
16117
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
16118
- const ok = !snap.running && snap.exitCode === 0;
16119
- const stillRunning = snap.running && !ok;
16120
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
16121
- try {
16122
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(snap.cursor), { recursive: true });
16123
- (0, import_node_fs45.writeFileSync)(snap.cursor, `${nextCursor}
16124
- `);
16125
- } catch {
16126
- }
16127
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
16128
- const progress = extra?.progress ?? snap.progress;
16129
- return {
16130
- stillRunning,
16131
- ok,
16132
- failed: Boolean(failed && !stillRunning && !ok),
16133
- status,
16134
- id,
16135
- pid: snap.pid,
16136
- exitCode: snap.exitCode ?? void 0,
16137
- phase: lastProgressLine(progress),
16138
- delta,
16139
- progress,
16140
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
16141
- };
16142
- }
16143
- async function sleepMs(ms) {
16144
- await new Promise((resolve2) => setTimeout(resolve2, ms));
16145
- }
16146
- async function waitForDetachedJob(cwd, id, opts) {
16147
- const jobId = sanitizeDetachedJobId(id);
16148
- const root = cwd.trim() || process.cwd();
16149
- const dir = resolveJobDir(root, jobId);
16150
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
16151
- const sleep = opts?.sleep ?? sleepMs;
16152
- if (!(0, import_node_fs45.existsSync)(dir)) {
16153
- return {
16154
- stillRunning: false,
16155
- ok: false,
16156
- failed: true,
16157
- status: "failed",
16158
- id: jobId,
16159
- delta: "",
16160
- progress: "No detached job. Start one first.",
16161
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
16162
- };
16163
- }
16164
- const deadline = Date.now() + timeoutMs;
16165
- let snap = snapshotJob(dir);
16166
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
16167
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
16168
- return toResult(jobId, snap, {
16169
- failed: true,
16170
- progress: "No detached job. Start one first."
16171
- });
16221
+ });
16222
+
16223
+ // src/git/clone-repo.ts
16224
+ async function cloneRepoIntoSideboard(opts) {
16225
+ const url = opts.url.trim();
16226
+ if (!url) throw new Error("Clone URL is required");
16227
+ let name = opts.name?.trim();
16228
+ if (!name) {
16229
+ const leaf = (0, import_node_path44.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
16230
+ name = leaf || "repo";
16172
16231
  }
16173
- while (Date.now() < deadline) {
16174
- snap = snapshotJob(dir);
16175
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
16176
- if (!snap.running) return toResult(jobId, snap, { failed: true });
16177
- await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
16232
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
16233
+ const dest = (0, import_node_path44.join)(sideboardReposDir(), name);
16234
+ if ((0, import_node_fs44.existsSync)(dest)) {
16235
+ const repoPath2 = await resolveRepoRoot(dest);
16236
+ const workspace2 = await ensureWorkspace(repoPath2);
16237
+ return { repoPath: repoPath2, workspace: workspace2 };
16178
16238
  }
16179
- snap = snapshotJob(dir);
16180
- return toResult(jobId, snap);
16239
+ const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
16240
+ if (result.exitCode !== 0) {
16241
+ throw new Error(
16242
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
16243
+ );
16244
+ }
16245
+ const repoPath = await resolveRepoRoot(dest);
16246
+ const workspace = await ensureWorkspace(repoPath);
16247
+ return { repoPath, workspace };
16181
16248
  }
16182
- function killProcessTree(pid, signal) {
16183
- if (!Number.isInteger(pid) || pid <= 0) return;
16184
- if (process.platform !== "win32") {
16185
- try {
16186
- process.kill(-pid, signal);
16187
- return;
16188
- } catch {
16189
- }
16249
+ var import_node_fs44, import_node_path44, import_execa6;
16250
+ var init_clone_repo = __esm({
16251
+ "src/git/clone-repo.ts"() {
16252
+ "use strict";
16253
+ import_node_fs44 = require("fs");
16254
+ import_node_path44 = require("path");
16255
+ import_execa6 = require("execa");
16256
+ init_paths();
16257
+ init_workspaces();
16258
+ init_worktree();
16259
+ }
16260
+ });
16261
+
16262
+ // src/types/thread.ts
16263
+ function isAdoptableRunScriptRequest(req) {
16264
+ return Boolean(req && !req.fulfilledAt && !req.error);
16265
+ }
16266
+ var init_thread = __esm({
16267
+ "src/types/thread.ts"() {
16268
+ "use strict";
16190
16269
  }
16270
+ });
16271
+
16272
+ // src/store/desktop-host.ts
16273
+ function desktopHostPidPath() {
16274
+ return (0, import_node_path45.join)(appDataDir(), "desktop-host.pid");
16275
+ }
16276
+ function readDesktopHostPid() {
16191
16277
  try {
16192
- process.kill(pid, signal);
16278
+ const pid = Number.parseInt((0, import_node_fs45.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
16279
+ if (!Number.isFinite(pid) || pid <= 0) return null;
16280
+ return pid;
16193
16281
  } catch {
16282
+ return null;
16194
16283
  }
16195
16284
  }
16196
- function appendJobLog(logFile, line) {
16285
+ function pidAlive(pid) {
16197
16286
  try {
16198
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(logFile), { recursive: true });
16199
- const body = line.endsWith("\n") ? line : `${line}
16200
- `;
16201
- (0, import_node_fs45.appendFileSync)(logFile, body);
16287
+ process.kill(pid, 0);
16288
+ return true;
16202
16289
  } catch {
16290
+ return false;
16203
16291
  }
16204
16292
  }
16205
- async function stopDetachedJob(cwd, id, opts) {
16206
- const jobId = sanitizeDetachedJobId(id);
16207
- const root = cwd.trim() || process.cwd();
16208
- const dir = resolveJobDir(root, jobId);
16209
- const sleep = opts?.sleep ?? sleepMs;
16210
- const why = opts?.reason?.trim() ?? "";
16211
- if (!(0, import_node_fs45.existsSync)(dir)) {
16212
- return {
16213
- stillRunning: false,
16214
- ok: false,
16215
- failed: true,
16216
- status: "failed",
16217
- id: jobId,
16218
- delta: "",
16219
- progress: "No detached job. Start one first.",
16220
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
16221
- stopped: false,
16222
- reason: "not-found"
16223
- };
16224
- }
16225
- let snap = snapshotJob(dir);
16226
- if (!snap.running) {
16227
- const result2 = toResult(jobId, snap);
16228
- return {
16229
- ...result2,
16230
- stopped: false,
16231
- reason: "not-running",
16232
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
16233
- };
16234
- }
16235
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
16236
- const targetPid = snap.pid;
16237
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
16238
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
16239
- const deadline = Date.now() + graceMs;
16240
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
16241
- await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
16242
- }
16243
- if (targetPid != null) {
16244
- killProcessTree(targetPid, "SIGKILL");
16245
- await sleep(100);
16246
- }
16247
- const exitFile = (0, import_node_path45.join)(dir, "exit");
16248
- if (!(0, import_node_fs45.existsSync)(exitFile)) {
16249
- try {
16250
- (0, import_node_fs45.writeFileSync)(exitFile, "1\n");
16251
- } catch {
16252
- }
16253
- }
16254
- snap = snapshotJob(dir);
16255
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
16256
- const result = toResult(jobId, snap, { failed: true });
16257
- const stopped = !stillRunning;
16258
- return {
16259
- ...result,
16260
- stillRunning,
16261
- ok: false,
16262
- failed: true,
16263
- status: stillRunning ? "running" : "failed",
16264
- stopped,
16265
- reason: stopped ? "stopped" : "still-running",
16266
- stopReason: why || void 0,
16267
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
16268
- };
16293
+ function isDesktopHostAlive() {
16294
+ const pid = readDesktopHostPid();
16295
+ return pid != null && pidAlive(pid);
16269
16296
  }
16270
- 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;
16271
- var init_wait_for_job = __esm({
16272
- "src/mcp/wait-for-job.ts"() {
16297
+ function isThisProcessDesktopHost() {
16298
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
16299
+ }
16300
+ function thisProcessShouldDrainAgentQueues() {
16301
+ if (isThisProcessDesktopHost()) return true;
16302
+ return !isDesktopHostAlive();
16303
+ }
16304
+ var import_node_fs45, import_node_path45;
16305
+ var init_desktop_host = __esm({
16306
+ "src/store/desktop-host.ts"() {
16273
16307
  "use strict";
16274
16308
  import_node_fs45 = require("fs");
16275
16309
  import_node_path45 = require("path");
16276
- init_workspace_scratch();
16277
- init_wait_for_turn();
16278
- MAX_JOB_CONTINUES = 8;
16279
- MAX_JOB_DELTA_LINES = 80;
16280
- MAX_JOB_DELTA_CHARS = 4e3;
16281
- DEFERRED_DONE_MAX_CHARS = 480;
16282
- GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
16283
- 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.";
16284
- 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.";
16285
- JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
16310
+ init_paths();
16311
+ }
16312
+ });
16313
+
16314
+ // src/orchestrator/child-halt.ts
16315
+ function isIncompleteChildStatus(status) {
16316
+ return HALT_STATUSES.has(status);
16317
+ }
16318
+ function childHaltNotice(child, status) {
16319
+ const title = child.title?.trim() || "Untitled";
16320
+ const link = `[${title}](sideboard://thread/${child.id})`;
16321
+ const why = child.lastError?.trim();
16322
+ const extra = why ? ` lastError: ${why}` : "";
16323
+ return [
16324
+ `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
16325
+ "This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
16326
+ ].join("\n");
16327
+ }
16328
+ function shouldNotifyParentAfterTurnError(opts) {
16329
+ return !opts.quotaFailoverHandled && !opts.crashContinued;
16330
+ }
16331
+ function shouldNotifyParentOfChildHalt(opts) {
16332
+ if (!isIncompleteChildStatus(opts.status)) return false;
16333
+ if (!opts.child.parentThreadId) return false;
16334
+ if (!opts.parent || opts.parent.status === "archived") return false;
16335
+ if (opts.parent.id === opts.child.id) return false;
16336
+ return isOrchestratorThread(opts.parent);
16337
+ }
16338
+ function noticeKey(childId, status) {
16339
+ return `${childId}:${status}`;
16340
+ }
16341
+ function notifyParentOfChildHalt(child, status, send) {
16342
+ const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
16343
+ if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
16344
+ const key = noticeKey(child.id, status);
16345
+ if (notified.has(key)) return false;
16346
+ notified.add(key);
16347
+ const parentId = parent.id;
16348
+ void send(parentId, childHaltNotice(child, status)).catch(() => {
16349
+ notified.delete(key);
16350
+ });
16351
+ return true;
16352
+ }
16353
+ var HALT_STATUSES, notified;
16354
+ var init_child_halt = __esm({
16355
+ "src/orchestrator/child-halt.ts"() {
16356
+ "use strict";
16357
+ init_global_workspace();
16358
+ init_thread_store();
16359
+ HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
16360
+ notified = /* @__PURE__ */ new Set();
16286
16361
  }
16287
16362
  });
16288
16363
 
@@ -21349,6 +21424,13 @@ function isPresentPlanToolName(name) {
21349
21424
  if (!name) return false;
21350
21425
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
21351
21426
  }
21427
+ function threadMayPresentPlan(thread) {
21428
+ return Boolean(thread?.planMode);
21429
+ }
21430
+ function shouldRegisterPresentPlan(thread) {
21431
+ if (thread == null) return true;
21432
+ return threadMayPresentPlan(thread);
21433
+ }
21352
21434
  function extractPresentedPlan(parts) {
21353
21435
  if (!parts?.length) return null;
21354
21436
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -21386,7 +21468,7 @@ function resolvePlanMarkdown(opts) {
21386
21468
  }
21387
21469
  return null;
21388
21470
  }
21389
- var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
21471
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
21390
21472
  var init_plan_present = __esm({
21391
21473
  "src/plan/plan-present.ts"() {
21392
21474
  "use strict";
@@ -21394,6 +21476,8 @@ var init_plan_present = __esm({
21394
21476
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
21395
21477
  PLAN_FILE_NAME = "plan.md";
21396
21478
  LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
21479
+ 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.";
21480
+ 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.";
21397
21481
  }
21398
21482
  });
21399
21483
 
@@ -21403,11 +21487,15 @@ __export(plan_file_exports, {
21403
21487
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
21404
21488
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
21405
21489
  PLAN_FILE_REL: () => PLAN_FILE_REL,
21490
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
21491
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
21406
21492
  extractPresentedPlan: () => extractPresentedPlan,
21407
21493
  isPresentPlanToolName: () => isPresentPlanToolName,
21408
21494
  planFileAbs: () => planFileAbs,
21409
21495
  readPlanFile: () => readPlanFile,
21410
21496
  resolvePlanMarkdown: () => resolvePlanMarkdown,
21497
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
21498
+ threadMayPresentPlan: () => threadMayPresentPlan,
21411
21499
  writePlanFile: () => writePlanFile
21412
21500
  });
21413
21501
  function ensureAttachmentsGitignore(worktreePath) {
@@ -23075,7 +23163,8 @@ var init_orchestrator = __esm({
23075
23163
  if (compact.didCompact) {
23076
23164
  thread = updateThread(threadId, {
23077
23165
  messages: compact.thread.messages,
23078
- sessionId: compact.thread.sessionId
23166
+ sessionId: compact.thread.sessionId,
23167
+ compactedMessages: compact.thread.compactedMessages
23079
23168
  });
23080
23169
  this.emit({
23081
23170
  type: "context_compacted",
@@ -23089,9 +23178,6 @@ var init_orchestrator = __esm({
23089
23178
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
23090
23179
  thread = this.requireThread(threadId);
23091
23180
  }
23092
- if (this.stoppedTurns.has(threadId)) {
23093
- return;
23094
- }
23095
23181
  const promptText = typeof prompt === "string" ? prompt : "";
23096
23182
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
23097
23183
  const parked = takePendingTurnAttachments(thread);
@@ -23108,6 +23194,14 @@ var init_orchestrator = __esm({
23108
23194
  agent: thread.agent,
23109
23195
  orch: isOrchestratorThread(thread)
23110
23196
  });
23197
+ if (this.stoppedTurns.has(threadId)) {
23198
+ const stopped = writeLiveStatus(threadId, "stopped");
23199
+ if (stopped?.status === "stopped") {
23200
+ this.emit({ type: "status_changed", threadId, status: "stopped" });
23201
+ }
23202
+ this.emit({ type: "turn_finished", threadId, exitCode: 1 });
23203
+ return;
23204
+ }
23111
23205
  thread = this.requireThread(threadId);
23112
23206
  const gitPrompt = expandCanonicalGitRequest(promptText);
23113
23207
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -23166,7 +23260,7 @@ var init_orchestrator = __esm({
23166
23260
  ].filter(Boolean).join("\n\n") || void 0;
23167
23261
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
23168
23262
  const agentPrompt = [
23169
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
23263
+ planModeTurnInstruction(thread.planMode),
23170
23264
  orchestrationReminder,
23171
23265
  prGateDirective,
23172
23266
  injectedNoticeContext,
@@ -23669,6 +23763,9 @@ var init_orchestrator = __esm({
23669
23763
  }
23670
23764
  const preferredPorts = active?.ports?.length ? active.ports : active?.port != null ? [active.port] : void 0;
23671
23765
  if (active && !existing) {
23766
+ if (typeof active.pid === "number") {
23767
+ killDetachedRunTree(active.pid, thread.worktreePath);
23768
+ }
23672
23769
  killListenersOnPorts(collectActiveRunPorts([active]));
23673
23770
  this.syncWorktreeRuns(
23674
23771
  thread.worktreePath,
@@ -23741,7 +23838,8 @@ var init_orchestrator = __esm({
23741
23838
  scriptName: resolvedName,
23742
23839
  port: handle.port,
23743
23840
  ports: handle.ports,
23744
- startedAt
23841
+ startedAt,
23842
+ ...typeof handle.pid === "number" && handle.pid > 1 ? { pid: handle.pid } : {}
23745
23843
  };
23746
23844
  const nextRuns = [
23747
23845
  ...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
@@ -23887,6 +23985,9 @@ var init_orchestrator = __esm({
23887
23985
  }
23888
23986
  const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
23889
23987
  if (!hadHandle2 && run2) {
23988
+ if (typeof run2.pid === "number") {
23989
+ killDetachedRunTree(run2.pid, thread.worktreePath);
23990
+ }
23890
23991
  killListenersOnPorts(collectActiveRunPorts([run2]));
23891
23992
  }
23892
23993
  const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
@@ -23908,6 +24009,11 @@ var init_orchestrator = __esm({
23908
24009
  }
23909
24010
  }
23910
24011
  if (!hadHandle && shared.activeRuns.length > 0) {
24012
+ for (const run2 of shared.activeRuns) {
24013
+ if (typeof run2.pid === "number") {
24014
+ killDetachedRunTree(run2.pid, thread.worktreePath);
24015
+ }
24016
+ }
23911
24017
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
23912
24018
  }
23913
24019
  this.syncWorktreeRuns(thread.worktreePath, [], null);
@@ -23949,6 +24055,11 @@ var init_orchestrator = __esm({
23949
24055
  (k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
23950
24056
  );
23951
24057
  if (!force && hasLiveHandle) continue;
24058
+ for (const run2 of shared.activeRuns) {
24059
+ if (typeof run2.pid === "number") {
24060
+ killDetachedRunTree(run2.pid, thread.worktreePath);
24061
+ }
24062
+ }
23952
24063
  killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
23953
24064
  if (shared.devPort != null) {
23954
24065
  killListenersOnPorts([shared.devPort]);
@@ -26577,6 +26688,7 @@ function mcpArchiveBlockedReason(thread) {
26577
26688
 
26578
26689
  // src/mcp/server.ts
26579
26690
  init_profile();
26691
+ init_plan_present();
26580
26692
 
26581
26693
  // src/mcp/run-script-ref.ts
26582
26694
  init_chat_tabs();
@@ -29507,31 +29619,40 @@ async function startMcpServer() {
29507
29619
  return mcpJson(payload);
29508
29620
  }
29509
29621
  );
29510
- server.tool(
29511
- "present_plan",
29512
- "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.",
29513
- {
29514
- title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
29515
- content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
29516
- thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
29517
- },
29518
- async ({ title, content, thread_id }) => {
29519
- const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
29520
- let root = process.cwd();
29521
- if (thread_id?.trim()) {
29522
- const t = orch.getThread(thread_id.trim());
29622
+ const callingThreadId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
29623
+ const callingThread = callingThreadId ? orch.getThread(callingThreadId) : null;
29624
+ if (shouldRegisterPresentPlan(callingThread)) {
29625
+ server.tool(
29626
+ "present_plan",
29627
+ PRESENT_PLAN_TOOL_DESCRIPTION,
29628
+ {
29629
+ title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
29630
+ content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
29631
+ thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
29632
+ },
29633
+ async ({ title, content, thread_id }) => {
29634
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
29635
+ const ref = thread_id?.trim() || callingThreadId;
29636
+ const t = ref ? orch.getThread(ref) : null;
29637
+ if (t && !threadMayPresentPlan(t)) {
29638
+ return mcpJson(
29639
+ { ok: false, error: PRESENT_PLAN_REQUIRES_PLAN_MODE },
29640
+ true
29641
+ );
29642
+ }
29643
+ let root = process.cwd();
29523
29644
  if (t?.worktreePath?.trim()) root = t.worktreePath;
29645
+ const path = writePlanFile2(root, content);
29646
+ const payload = {
29647
+ ok: true,
29648
+ path,
29649
+ title: title?.trim() || "Plan",
29650
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
29651
+ };
29652
+ return mcpJson(payload);
29524
29653
  }
29525
- const path = writePlanFile2(root, content);
29526
- const payload = {
29527
- ok: true,
29528
- path,
29529
- title: title?.trim() || "Plan",
29530
- message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
29531
- };
29532
- return mcpJson(payload);
29533
- }
29534
- );
29654
+ );
29655
+ }
29535
29656
  server.tool(
29536
29657
  "present_schema",
29537
29658
  "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.",