@sideboard-ai/core 0.1.242 → 0.1.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{abletime-VSV6AUBV.js → abletime-CWIJ7INC.js} +4 -4
  2. package/dist/{abletime-UX4VUENQ.js → abletime-OI6FNSXX.js} +4 -4
  3. package/dist/{abletime-oauth-DKBUZV2C.js → abletime-oauth-CHVTUZBR.js} +3 -3
  4. package/dist/{abletime-oauth-QMDV45JC.js → abletime-oauth-PUIPZTA4.js} +3 -3
  5. package/dist/{abletime-rest-NL56UVK5.js → abletime-rest-32NIVXAY.js} +3 -3
  6. package/dist/{abletime-rest-VPBC2XS5.js → abletime-rest-APJUKFKL.js} +3 -3
  7. package/dist/{agents-CDQCQVWU.js → agents-CPME6O2H.js} +9 -5
  8. package/dist/{agents-PBRIHUYM.js → agents-VKO5UZO6.js} +9 -5
  9. package/dist/{app-settings-AGDLF75Z.js → app-settings-652J57CX.js} +1 -1
  10. package/dist/{app-settings-I27UBVI4.js → app-settings-VOPXJMGV.js} +1 -1
  11. package/dist/{chunk-BILEJG3V.js → chunk-3P5OCI2X.js} +3 -3
  12. package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
  13. package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
  14. package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
  15. package/dist/{chunk-TB6UMHIF.js → chunk-7UCZ4DRF.js} +2 -2
  16. package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
  17. package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
  18. package/dist/{chunk-O3UAQAYA.js → chunk-CCD2QIFK.js} +2 -2
  19. package/dist/{chunk-LRMJ6V6U.js → chunk-DARSRQUE.js} +1503 -1462
  20. package/dist/{chunk-OKHOYI53.js → chunk-ECZTXNYR.js} +3 -3
  21. package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
  22. package/dist/{chunk-3Q4PVZ3U.js → chunk-HR4RNC73.js} +32 -5
  23. package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
  24. package/dist/{chunk-QXYWLO6W.js → chunk-ISZERVDT.js} +28 -5
  25. package/dist/{chunk-R3YKDTVE.js → chunk-NEOAHPMK.js} +1569 -1526
  26. package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
  27. package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
  28. package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
  29. package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
  30. package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
  31. package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
  32. package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-AJ3E6LBX.js} +4 -4
  33. package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-PYIRQGU7.js} +4 -4
  34. package/dist/{global-workspace-TD5SARNW.js → global-workspace-GIQ6F6IJ.js} +4 -4
  35. package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-GQM653DP.js} +4 -4
  36. package/dist/index.cjs +574 -464
  37. package/dist/index.d.cts +43 -3
  38. package/dist/index.d.ts +43 -3
  39. package/dist/index.js +70 -38
  40. package/dist/mcp/run-stdio.cjs +1633 -1544
  41. package/dist/mcp/run-stdio.js +50 -36
  42. package/dist/{orchestrator-66ZFQHW5.js → orchestrator-3IOISDFS.js} +7 -7
  43. package/dist/{orchestrator-VYNPNHY4.js → orchestrator-YDPDMXUY.js} +7 -7
  44. package/dist/{plan-file-UUKCZEYP.js → plan-file-7MMIPV7Y.js} +9 -1
  45. package/dist/{plan-file-2EPHYRX7.js → plan-file-CKXUFTQ2.js} +9 -1
  46. package/dist/{thread-store-YOPKY74K.js → thread-store-JQB7KZJR.js} +1 -1
  47. package/dist/{thread-store-AYPBEZT5.js → thread-store-SCV2IZYS.js} +1 -1
  48. package/dist/{workspaces-4SFSCYKT.js → workspaces-OIBSD5B6.js} +4 -4
  49. package/dist/{workspaces-JX5R4V24.js → workspaces-WYC5TLC3.js} +4 -4
  50. package/dist/{worktree-Y5FES5D6.js → worktree-5VRZVGP3.js} +3 -3
  51. package/dist/{worktree-AGVPKDBH.js → worktree-DGDM7LGV.js} +3 -3
  52. package/package.json +1 -1
@@ -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,1708 +14650,1714 @@ 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
- }
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);
14715
14718
  }
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;
14719
+ function jobTreeAlive(pid) {
14720
+ if (pid == null) return false;
14721
+ return jobAlive(pid) || processGroupAlive(pid);
14724
14722
  }
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
- }
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;
14736
14727
  }
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;
14728
+ function jobDir(root, id, legacy = false) {
14729
+ return (0, import_node_path39.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
14751
14730
  }
14752
- function realpathOrResolve(path) {
14753
- try {
14754
- return (0, import_node_fs39.realpathSync)(path);
14755
- } catch {
14756
- return (0, import_node_path39.resolve)(path);
14757
- }
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;
14758
14737
  }
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;
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);
14769
14742
  }
14770
- function signalPid(pid, signal) {
14771
- try {
14772
- process.kill(pid, signal);
14773
- } catch {
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);
14774
14755
  }
14756
+ return running.sort();
14775
14757
  }
14776
- function isThisWorktreeListenerCwd(cwd, worktreePath) {
14777
- const wt = realpathOrResolve(worktreePath);
14778
- const c = realpathOrResolve(cwd);
14779
- return c === wt || c.startsWith(wt + import_node_path39.sep);
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);
14780
14764
  }
14781
- function killDetachedRunTree(pid, worktreePath) {
14782
- if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
14783
- const cwd = listenerCwd(pid);
14784
- if (!cwd || !isThisWorktreeListenerCwd(cwd, worktreePath)) return;
14785
- try {
14786
- process.kill(-pid, "SIGTERM");
14787
- } catch {
14788
- signalPid(pid, "SIGTERM");
14789
- }
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");
14790
14769
  }
14791
- function killListenersOnPorts(ports) {
14792
- const selfPid = process.pid;
14793
- for (const port of ports) {
14794
- if (!Number.isFinite(port) || port <= 0) continue;
14795
- try {
14796
- if (process.platform === "win32") {
14797
- (0, import_node_child_process4.execFileSync)(
14798
- "powershell",
14799
- [
14800
- "-NoProfile",
14801
- "-Command",
14802
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
14803
- ],
14804
- { stdio: "ignore", timeout: 3e3 }
14805
- );
14806
- continue;
14807
- }
14808
- for (const pid of listUnixListenerPids(port, selfPid)) {
14809
- signalPid(pid, "SIGTERM");
14810
- }
14811
- } catch {
14812
- }
14813
- }
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");
14814
14781
  }
14815
- function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
14816
- if (!worktreePath.trim()) {
14817
- killListenersOnPorts(ports);
14818
- return;
14819
- }
14820
- if (process.platform === "win32") {
14821
- killListenersOnPorts(ports);
14822
- return;
14823
- }
14824
- const selfPid = process.pid;
14825
- for (const port of ports) {
14826
- if (!Number.isFinite(port) || port <= 0) continue;
14827
- for (const pid of listUnixListenerPids(port, selfPid)) {
14828
- const cwd = listenerCwd(pid);
14829
- if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
14830
- signalPid(pid, signal);
14831
- }
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)}`;
14832
14790
  }
14791
+ return out;
14833
14792
  }
14834
- function killScriptTree(child, ports = []) {
14835
- const pid = child.pid;
14836
- if (pid) {
14837
- try {
14838
- if (process.platform === "win32") {
14839
- void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
14840
- } else {
14841
- process.kill(-pid, "SIGTERM");
14842
- setTimeout(() => {
14843
- try {
14844
- process.kill(-pid, "SIGKILL");
14845
- } catch {
14846
- }
14847
- }, 2500).unref?.();
14848
- }
14849
- } catch {
14850
- try {
14851
- child.kill("SIGTERM");
14852
- } catch {
14853
- }
14854
- }
14855
- }
14856
- killListenersOnPorts(ports);
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(" ");
14857
14801
  }
14858
- async function spawnWorkspaceScript(command, opts) {
14859
- const loginEnv = await captureLoginEnv();
14860
- const env = buildWorkspaceScriptEnv(
14861
- {
14862
- worktreePath: opts.worktreePath,
14863
- repoPath: opts.repoPath,
14864
- workspaceName: opts.workspaceName,
14865
- defaultBranch: opts.defaultBranch,
14866
- ports: opts.ports
14867
- },
14868
- loginEnv
14869
- );
14870
- try {
14871
- mergeAgentGitAuthEnv(
14872
- env,
14873
- await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
14874
- );
14875
- } catch {
14876
- }
14877
- if (opts.heldPorts?.length) {
14878
- await Promise.all(opts.heldPorts.map((h) => h.release()));
14879
- }
14880
- const shell = process.platform === "darwin" ? "zsh" : "bash";
14881
- const child = (0, import_execa4.execa)(shell, ["-lc", command], {
14882
- cwd: opts.worktreePath,
14883
- reject: false,
14884
- env,
14885
- // Own process group so Stop can tear down the whole tree (not just the shell).
14886
- // There is no settings.toml `stop=` / teardown hook for run scripts.
14887
- ...process.platform === "win32" ? {} : { detached: true }
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);
14888
14815
  });
14889
- pipeLines(child.stdout, opts.onLine);
14890
- pipeLines(child.stderr, opts.onLine);
14891
- const ports = opts.ports ?? [];
14892
- return {
14893
- pid: child.pid,
14894
- kill: () => killScriptTree(child, ports),
14895
- done: child.then((r) => r.exitCode ?? null),
14896
- child
14897
- };
14898
14816
  }
14899
- async function attachAbort(handle, signal) {
14900
- if (signal) {
14901
- const onAbort = () => handle.kill();
14902
- if (signal.aborted) onAbort();
14903
- else signal.addEventListener("abort", onAbort, { once: true });
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
+ };
14904
14829
  }
14905
- return handle.done;
14830
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
14831
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
14832
+ }
14833
+ return { action: "none" };
14906
14834
  }
14907
- async function runSetupScript(repoPath, worktreePath, onLine, opts) {
14908
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14909
- if (!settings?.setup) return { ran: false, exitCode: null, source: null };
14910
- const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
14911
- onLine?.(`[setup] ${source ?? "settings.toml"}`);
14912
- const handle = await spawnWorkspaceScript(settings.setup, {
14913
- worktreePath,
14914
- repoPath,
14915
- defaultBranch: opts?.defaultBranch,
14916
- onLine
14917
- });
14918
- const exitCode = await attachAbort(handle, opts?.signal);
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);
14919
14859
  return {
14920
- ran: true,
14921
- exitCode,
14922
- source,
14923
- kill: handle.kill
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"))
14924
14866
  };
14925
14867
  }
14926
- async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
14927
- const found = findConventionSetup(worktreePath, repoPath);
14928
- if (!found) return { ran: false, exitCode: null, source: null };
14929
- onLine?.(`[setup] ${found.source}`);
14930
- const handle = await spawnWorkspaceScript(found.command, {
14931
- worktreePath,
14932
- repoPath,
14933
- defaultBranch: opts?.defaultBranch,
14934
- onLine
14935
- });
14936
- const exitCode = await attachAbort(handle, opts?.signal);
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;
14937
14881
  return {
14938
- ran: true,
14939
- exitCode,
14940
- source: found.source,
14941
- kill: handle.kill
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
14942
14893
  };
14943
14894
  }
14944
- async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
14945
- await ensureWorktreeSideboardIgnored(worktreePath);
14946
- const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
14947
- if (review.wrote) {
14948
- onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
14949
- }
14950
- let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
14951
- if (!setup.ran) {
14952
- setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
14953
- }
14954
- if (!setup.ran) {
14955
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
14956
- }
14957
- return setup;
14895
+ async function sleepMs(ms) {
14896
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
14958
14897
  }
14959
- async function runArchiveScript(repoPath, worktreePath, onLine) {
14960
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14961
- if (!settings?.archive) return { ran: false, exitCode: null };
14962
- const handle = await spawnWorkspaceScript(settings.archive, {
14963
- worktreePath,
14964
- repoPath,
14965
- onLine
14966
- });
14967
- const exitCode = await handle.done;
14968
- return { ran: true, exitCode };
14969
- }
14970
- function listRunScripts(worktreePath, repoPath) {
14971
- const settings = loadWorkspaceSettings(worktreePath, repoPath);
14972
- if (!settings?.runScripts.length) return [];
14973
- return settings.runScripts.filter((s) => {
14974
- if (!s.availableIn?.length) return true;
14975
- return s.availableIn.includes("local");
14976
- });
14977
- }
14978
- function getDefaultRunScript(worktreePath, repoPath) {
14979
- const scripts = listRunScripts(worktreePath, repoPath);
14980
- if (!scripts.length) return null;
14981
- return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
14982
- }
14983
- function getRunScript(worktreePath, repoPath, name) {
14984
- const scripts = listRunScripts(worktreePath, repoPath);
14985
- if (!scripts.length) return null;
14986
- if (name) {
14987
- return scripts.find((s) => s.name === name) ?? null;
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
+ };
14988
14915
  }
14989
- return getDefaultRunScript(worktreePath, repoPath);
14990
- }
14991
- function getRunMode(worktreePath, repoPath) {
14992
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
14993
- }
14994
- async function reservePort() {
14995
- return new Promise((resolve2, reject) => {
14996
- const server = (0, import_node_net.createServer)();
14997
- let released = false;
14998
- const release = () => new Promise((res) => {
14999
- if (released) {
15000
- res();
15001
- return;
15002
- }
15003
- released = true;
15004
- server.close(() => res());
15005
- });
15006
- server.listen(0, "127.0.0.1", () => {
15007
- const addr = server.address();
15008
- if (!addr || typeof addr === "string") {
15009
- void release().then(() => reject(new Error("Failed to allocate port")));
15010
- return;
15011
- }
15012
- resolve2({ port: addr.port, release });
15013
- });
15014
- server.on("error", reject);
15015
- });
15016
- }
15017
- async function tryReservePort(port) {
15018
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
15019
- return new Promise((resolve2) => {
15020
- const server = (0, import_node_net.createServer)();
15021
- let released = false;
15022
- const release = () => new Promise((res) => {
15023
- if (released) {
15024
- res();
15025
- return;
15026
- }
15027
- released = true;
15028
- server.close(() => res());
15029
- });
15030
- server.once("error", () => {
15031
- try {
15032
- server.close();
15033
- } catch {
15034
- }
15035
- resolve2(null);
15036
- });
15037
- server.listen(port, "127.0.0.1", () => {
15038
- resolve2({ port, release });
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."
15039
14923
  });
15040
- });
15041
- }
15042
- function normalizePreferredPorts(ports, size) {
15043
- if (!ports?.length) return [];
15044
- const seen = /* @__PURE__ */ new Set();
15045
- const out = [];
15046
- for (const p of ports) {
15047
- if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
15048
- seen.add(p);
15049
- out.push(p);
15050
- if (out.length >= size) break;
15051
14924
  }
15052
- return out;
15053
- }
15054
- function delay(ms) {
15055
- return new Promise((resolve2) => setTimeout(resolve2, ms));
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())));
14930
+ }
14931
+ snap = snapshotJob(dir);
14932
+ return toResult(jobId, snap);
15056
14933
  }
15057
- async function fillPortRange(held, size) {
15058
- if (held.length >= size) return held;
15059
- const base = held[0]?.port;
15060
- const taken = new Set(held.map((h) => h.port));
15061
- try {
15062
- let offset = 0;
15063
- while (held.length < size) {
15064
- const candidate = base != null ? base + offset : null;
15065
- offset += 1;
15066
- if (candidate != null && taken.has(candidate)) continue;
15067
- const next = candidate != null ? await tryReservePort(candidate) : null;
15068
- if (next) {
15069
- held.push(next);
15070
- taken.add(next.port);
15071
- } else {
15072
- const ephemeral = await reservePort();
15073
- held.push(ephemeral);
15074
- taken.add(ephemeral.port);
15075
- }
14934
+ function killProcessTree(pid, signal) {
14935
+ if (!Number.isInteger(pid) || pid <= 0) return;
14936
+ if (process.platform !== "win32") {
14937
+ try {
14938
+ process.kill(-pid, signal);
14939
+ return;
14940
+ } catch {
15076
14941
  }
15077
- return held;
15078
- } catch (err) {
15079
- await Promise.all(held.map((h) => h.release()));
15080
- throw err;
15081
14942
  }
15082
- }
15083
- async function tryReserveAvailablePorts(ports) {
15084
- const held = [];
15085
- for (const port of ports) {
15086
- const next = await tryReservePort(port);
15087
- if (next) held.push(next);
14943
+ try {
14944
+ process.kill(pid, signal);
14945
+ } catch {
15088
14946
  }
15089
- return held;
15090
14947
  }
15091
- async function reclaimPreferredPorts(ports, worktreePath) {
15092
- const primary = ports[0];
15093
- if (primary == null) return null;
15094
- const takePrimary = async () => {
15095
- const held2 = await tryReserveAvailablePorts(ports);
15096
- if (held2.some((h) => h.port === primary)) return held2;
15097
- await Promise.all(held2.map((h) => h.release()));
15098
- return null;
15099
- };
15100
- let held = await takePrimary();
15101
- if (held) return held;
15102
- if (worktreePath) {
15103
- killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
15104
- } else {
15105
- killListenersOnPorts(ports);
15106
- }
15107
- for (let i = 0; i < RECLAIM_TRIES; i++) {
15108
- await delay(RECLAIM_RETRY_DELAY_MS);
15109
- held = await takePrimary();
15110
- if (held) return held;
15111
- if (i === 2 && worktreePath) {
15112
- killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
15113
- }
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 {
15114
14955
  }
15115
- return null;
15116
14956
  }
15117
- async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
15118
- const preferred = normalizePreferredPorts(opts?.preferred, size);
15119
- if (preferred.length) {
15120
- const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
15121
- if (reused) return fillPortRange(reused, size);
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
+ };
15122
14976
  }
15123
- const held = [await reservePort()];
15124
- return fillPortRange(held, size);
15125
- }
15126
- async function startDevServer(repoPath, worktreePath, onLine, opts) {
15127
- const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
15128
- if (!script) return null;
15129
- const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
15130
- const held = await reservePortRange(PORT_RANGE_SIZE, {
15131
- preferred: preferred ?? void 0,
15132
- worktreePath
15133
- });
15134
- const ports = held.map((h) => h.port);
15135
- saveWorktreeRunPorts(worktreePath, script.name, ports);
15136
- try {
15137
- const handle = await spawnWorkspaceScript(script.command, {
15138
- worktreePath,
15139
- repoPath,
15140
- ports,
15141
- heldPorts: held,
15142
- defaultBranch: opts?.defaultBranch,
15143
- onLine
15144
- });
14977
+ let snap = snapshotJob(dir);
14978
+ if (!snap.running) {
14979
+ const result2 = toResult(jobId, snap);
15145
14980
  return {
15146
- pid: handle.pid,
15147
- port: ports[0],
15148
- ports,
15149
- scriptName: script.name,
15150
- kill: handle.kill,
15151
- done: handle.done
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."
15152
14985
  };
15153
- } catch (err) {
15154
- await Promise.all(held.map((h) => h.release()));
15155
- throw err;
15156
14986
  }
15157
- }
15158
- 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;
15159
- var init_conductor = __esm({
15160
- "src/hook/conductor.ts"() {
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;
15010
+ return {
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
15020
+ };
15021
+ }
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"() {
15161
15025
  "use strict";
15162
15026
  import_node_fs39 = require("fs");
15163
- import_node_child_process4 = require("child_process");
15164
- import_node_net = require("net");
15165
15027
  import_node_path39 = require("path");
15166
- import_execa4 = require("execa");
15167
- import_node_readline3 = require("readline");
15168
- init_settings();
15169
- init_nested_electron_env();
15170
- init_worktree_pkg_cache();
15171
- init_convention_setup();
15172
- init_cursor_worktrees();
15173
- init_git_auth_mode();
15174
- init_request_review();
15175
- init_worktree_exclude();
15176
- init_worktree_run_ports();
15177
- init_worktree_run_ports();
15178
- init_nested_electron_env();
15179
- PORT_RANGE_SIZE = 10;
15180
- RECLAIM_RETRY_DELAY_MS = 40;
15181
- RECLAIM_TRIES = 8;
15182
- cachedLoginEnv = null;
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}$/;
15183
15038
  }
15184
15039
  });
15185
15040
 
15186
- // src/store/setup-log.ts
15187
- function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
15188
- const next = prev ? `${prev}
15189
- ${line}` : line;
15190
- if (next.length <= max) return next;
15191
- const cut = next.slice(next.length - max);
15192
- const nl = cut.indexOf("\n");
15193
- return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
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);
15194
15045
  }
15195
- function fromSnapshot(snap) {
15196
- return {
15197
- chunks: snap.output ? [snap.output] : [],
15198
- length: snap.output.length,
15199
- running: snap.running,
15200
- exitCode: snap.exitCode,
15201
- source: snap.source
15202
- };
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("#"));
15203
15050
  }
15204
- function materialize(mem) {
15205
- let output = mem.chunks.join("\n");
15206
- if (output.length > MAX_SETUP_LOG_CHARS) {
15207
- output = appendSetupOutput("", output);
15208
- mem.chunks = [output];
15209
- mem.length = output.length;
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)];
15210
15071
  }
15211
- return {
15212
- output,
15213
- running: mem.running,
15214
- exitCode: mem.exitCode,
15215
- source: mem.source
15216
- };
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 {
15080
+ }
15081
+ if (defaults.length) return defaults;
15082
+ return [".env.local", ".env"];
15217
15083
  }
15218
- function setupLogPath(threadId) {
15219
- return (0, import_node_path40.join)(threadsDir(), `${threadId}.setup.log.json`);
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);
15094
+ }
15095
+ return copied;
15220
15096
  }
15221
- function setupLogKeyForWorktree(worktreePath) {
15222
- const norm = normalizeWorktreePath(worktreePath);
15223
- return `wt-${(0, import_node_crypto8.createHash)("sha1").update(norm).digest("hex").slice(0, 16)}`;
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 };
15224
15122
  }
15225
- function emptySetupLog() {
15226
- return { ...EMPTY };
15123
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
15124
+ const env = stripNestedElectronEnv({
15125
+ ...baseEnv ?? process.env
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;
15227
15152
  }
15228
- function persistNow(threadId) {
15229
- const timer = persistTimers.get(threadId);
15230
- if (timer) {
15231
- clearTimeout(timer);
15232
- persistTimers.delete(threadId);
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);
15157
+ }
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);
15233
15164
  }
15234
- const mem = memory.get(threadId);
15235
- if (!mem) return;
15165
+ return pids;
15166
+ }
15167
+ function listUnixListenerPids(port, selfPid) {
15236
15168
  try {
15237
- (0, import_node_fs40.writeFileSync)(
15238
- setupLogPath(threadId),
15239
- `${JSON.stringify(materialize(mem))}
15240
- `,
15241
- "utf8"
15242
- );
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);
15243
15175
  } catch {
15176
+ return [];
15244
15177
  }
15245
15178
  }
15246
- function schedulePersist(threadId) {
15247
- if (persistTimers.has(threadId)) return;
15248
- persistTimers.set(
15249
- threadId,
15250
- setTimeout(() => {
15251
- persistTimers.delete(threadId);
15252
- persistNow(threadId);
15253
- }, PERSIST_DEBOUNCE_MS)
15254
- );
15255
- }
15256
- function loadFromDisk(threadId) {
15257
- const path = setupLogPath(threadId);
15258
- if (!(0, import_node_fs40.existsSync)(path)) return null;
15179
+ function listenerCwd(pid) {
15180
+ if (!Number.isFinite(pid) || pid <= 0) return null;
15259
15181
  try {
15260
- const raw = JSON.parse((0, import_node_fs40.readFileSync)(path, "utf8"));
15261
- return {
15262
- output: typeof raw.output === "string" ? raw.output : "",
15263
- running: Boolean(raw.running),
15264
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15265
- source: typeof raw.source === "string" ? raw.source : null
15266
- };
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
+ }
15267
15190
  } catch {
15268
- return null;
15269
15191
  }
15192
+ return null;
15270
15193
  }
15271
- function memFor(threadId) {
15272
- const cached = memory.get(threadId);
15273
- if (cached) return cached;
15274
- const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
15275
- memory.set(threadId, mem);
15276
- return mem;
15277
- }
15278
- function readSetupLog(threadId) {
15279
- const cached = memory.get(threadId);
15280
- if (cached) return materialize(cached);
15281
- const disk = loadFromDisk(threadId);
15282
- if (disk) {
15283
- memory.set(threadId, fromSnapshot(disk));
15284
- return disk;
15285
- }
15286
- return emptySetupLog();
15287
- }
15288
- function beginSetupLog(threadId) {
15289
- const mem = {
15290
- chunks: [],
15291
- length: 0,
15292
- running: true,
15293
- exitCode: null,
15294
- source: null
15295
- };
15296
- memory.set(threadId, mem);
15297
- persistNow(threadId);
15298
- return materialize(mem);
15299
- }
15300
- function appendSetupLog(threadId, line) {
15301
- const mem = memFor(threadId);
15302
- mem.chunks.push(line);
15303
- mem.length += line.length + 1;
15304
- mem.running = true;
15305
- if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
15306
- schedulePersist(threadId);
15307
- }
15308
- function finishSetupLog(threadId, exitCode, source) {
15309
- const mem = memFor(threadId);
15310
- mem.running = false;
15311
- mem.exitCode = exitCode;
15312
- mem.source = source ?? mem.source;
15313
- persistNow(threadId);
15314
- return materialize(mem);
15315
- }
15316
- var import_node_crypto8, import_node_fs40, import_node_path40, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
15317
- var init_setup_log = __esm({
15318
- "src/store/setup-log.ts"() {
15319
- "use strict";
15320
- import_node_crypto8 = require("crypto");
15321
- import_node_fs40 = require("fs");
15322
- import_node_path40 = require("path");
15323
- init_worktree_labels();
15324
- init_paths();
15325
- EMPTY = {
15326
- output: "",
15327
- running: false,
15328
- exitCode: null,
15329
- source: null
15330
- };
15331
- MAX_SETUP_LOG_CHARS = 256e3;
15332
- memory = /* @__PURE__ */ new Map();
15333
- persistTimers = /* @__PURE__ */ new Map();
15334
- PERSIST_DEBOUNCE_MS = 500;
15335
- }
15336
- });
15337
-
15338
- // src/store/run-log.ts
15339
- function fromSnapshot2(snap) {
15340
- return {
15341
- chunks: snap.output ? [snap.output] : [],
15342
- length: snap.output.length,
15343
- running: snap.running,
15344
- exitCode: snap.exitCode,
15345
- scriptName: snap.scriptName
15346
- };
15347
- }
15348
- function materialize2(mem) {
15349
- let output = mem.chunks.join("\n");
15350
- if (output.length > MAX_RUN_LOG_CHARS) {
15351
- output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
15352
- mem.chunks = [output];
15353
- mem.length = output.length;
15354
- }
15355
- return {
15356
- output,
15357
- running: mem.running,
15358
- exitCode: mem.exitCode,
15359
- scriptName: mem.scriptName
15360
- };
15361
- }
15362
- function safeScriptToken(scriptName) {
15363
- const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
15364
- return token || "dev";
15365
- }
15366
- function runLogPath(key) {
15367
- return (0, import_node_path41.join)(threadsDir(), `${key}.run.log.json`);
15368
- }
15369
- function runLogKeyForWorktree(worktreePath, scriptName) {
15370
- const norm = normalizeWorktreePath(worktreePath);
15371
- const hash = (0, import_node_crypto9.createHash)("sha1").update(norm).digest("hex").slice(0, 16);
15372
- return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
15373
- }
15374
- function emptyRunLog() {
15375
- return { ...EMPTY2 };
15376
- }
15377
- function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
15378
- const max = Math.max(1, tailChars);
15379
- const outputChars = output.length;
15380
- if (outputChars <= max) return { output, truncated: false, outputChars };
15381
- const cut = output.slice(outputChars - max);
15382
- const nl = cut.indexOf("\n");
15383
- const text7 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
15384
- return { output: text7, truncated: true, outputChars };
15385
- }
15386
- function persistNow2(key) {
15387
- const timer = persistTimers2.get(key);
15388
- if (timer) {
15389
- clearTimeout(timer);
15390
- persistTimers2.delete(key);
15391
- }
15392
- const mem = memory2.get(key);
15393
- if (!mem) return;
15194
+ function realpathOrResolve(path) {
15394
15195
  try {
15395
- (0, import_node_fs41.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15396
- `, "utf8");
15196
+ return (0, import_node_fs40.realpathSync)(path);
15397
15197
  } catch {
15198
+ return (0, import_node_path40.resolve)(path);
15398
15199
  }
15399
15200
  }
15400
- function schedulePersist2(key) {
15401
- if (persistTimers2.has(key)) return;
15402
- persistTimers2.set(
15403
- key,
15404
- setTimeout(() => {
15405
- persistTimers2.delete(key);
15406
- persistNow2(key);
15407
- }, PERSIST_DEBOUNCE_MS2)
15408
- );
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;
15409
15211
  }
15410
- function loadFromDisk2(key) {
15411
- const path = runLogPath(key);
15412
- if (!(0, import_node_fs41.existsSync)(path)) return null;
15212
+ function signalPid(pid, signal) {
15413
15213
  try {
15414
- const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
15415
- return {
15416
- output: typeof raw.output === "string" ? raw.output : "",
15417
- running: Boolean(raw.running),
15418
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15419
- scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
15420
- };
15214
+ process.kill(pid, signal);
15421
15215
  } catch {
15422
- return null;
15423
15216
  }
15424
15217
  }
15425
- function memFor2(key) {
15426
- const cached = memory2.get(key);
15427
- if (cached) return cached;
15428
- const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
15429
- memory2.set(key, mem);
15430
- return mem;
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);
15431
15222
  }
15432
- function readRunLog(key) {
15433
- const cached = memory2.get(key);
15434
- if (cached) return materialize2(cached);
15435
- const disk = loadFromDisk2(key);
15436
- if (disk) {
15437
- memory2.set(key, fromSnapshot2(disk));
15438
- return disk;
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;
15439
15230
  }
15440
- return emptyRunLog();
15441
- }
15442
- function beginRunLog(key, scriptName) {
15443
- const mem = {
15444
- chunks: [],
15445
- length: 0,
15446
- running: true,
15447
- exitCode: null,
15448
- scriptName
15449
- };
15450
- memory2.set(key, mem);
15451
- persistNow2(key);
15452
- return materialize2(mem);
15453
- }
15454
- function appendRunLog(key, line) {
15455
- const mem = memFor2(key);
15456
- mem.chunks.push(line);
15457
- mem.length += line.length + 1;
15458
- mem.running = true;
15459
- if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
15460
- schedulePersist2(key);
15461
- }
15462
- function finishRunLog(key, exitCode) {
15463
- const mem = memFor2(key);
15464
- mem.running = false;
15465
- mem.exitCode = exitCode;
15466
- persistNow2(key);
15467
- return materialize2(mem);
15468
- }
15469
- function flushRunLog(key) {
15470
- persistNow2(key);
15471
- }
15472
- 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;
15473
- var init_run_log = __esm({
15474
- "src/store/run-log.ts"() {
15475
- "use strict";
15476
- import_node_crypto9 = require("crypto");
15477
- import_node_fs41 = require("fs");
15478
- import_node_path41 = require("path");
15479
- init_worktree_labels();
15480
- init_setup_log();
15481
- init_paths();
15482
- EMPTY2 = {
15483
- output: "",
15484
- running: false,
15485
- exitCode: null,
15486
- scriptName: null
15487
- };
15488
- MAX_RUN_LOG_CHARS = 256e3;
15489
- DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
15490
- memory2 = /* @__PURE__ */ new Map();
15491
- persistTimers2 = /* @__PURE__ */ new Map();
15492
- PERSIST_DEBOUNCE_MS2 = 500;
15231
+ try {
15232
+ process.kill(-pid, "SIGTERM");
15233
+ } catch {
15234
+ signalPid(pid, "SIGTERM");
15493
15235
  }
15494
- });
15495
-
15496
- // src/git/orphan-cleanup.ts
15497
- function isSideboardWorktreePath(path) {
15498
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
15499
15236
  }
15500
- async function findOrphanWorktrees(repoPaths) {
15501
- const threads = listThreads({ includeArchived: true });
15502
- const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
15503
- const repos = new Set(
15504
- repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
15505
- );
15506
- const homeRoot = sideboardWorkspacesDir();
15507
- if ((0, import_node_fs42.existsSync)(homeRoot)) {
15237
+ function killListenersOnPorts(ports) {
15238
+ const selfPid = process.pid;
15239
+ for (const port of ports) {
15240
+ if (!Number.isFinite(port) || port <= 0) continue;
15508
15241
  try {
15509
- for (const entry of (0, import_node_fs42.readdirSync)(homeRoot, { withFileTypes: true })) {
15510
- if (!entry.isDirectory()) continue;
15511
- void entry;
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");
15512
15256
  }
15513
15257
  } catch {
15514
15258
  }
15515
15259
  }
15516
- const orphans = [];
15517
- const seen = /* @__PURE__ */ new Set();
15518
- for (const repoPath of repos) {
15519
- if (!repoPath || !(0, import_node_fs42.existsSync)(repoPath)) continue;
15520
- try {
15521
- const wts = await listWorktrees(repoPath);
15522
- for (const wt of wts) {
15523
- const path = wt.path.replace(/\/$/, "");
15524
- if (!isSideboardWorktreePath(path)) continue;
15525
- if (known.has(path)) continue;
15526
- if (seen.has(path)) continue;
15527
- seen.add(path);
15528
- let mtimeMs = 0;
15529
- try {
15530
- mtimeMs = (0, import_node_fs42.statSync)(path).mtimeMs;
15531
- } catch {
15532
- mtimeMs = 0;
15533
- }
15534
- orphans.push({ path, repoPath, mtimeMs });
15535
- }
15536
- } catch {
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);
15537
15277
  }
15278
+ }
15279
+ }
15280
+ function killScriptTree(child, ports = []) {
15281
+ const pid = child.pid;
15282
+ if (pid) {
15538
15283
  try {
15539
- const root = worktreesRoot(repoPath);
15540
- if ((0, import_node_fs42.existsSync)(root)) {
15541
- for (const entry of (0, import_node_fs42.readdirSync)(root, { withFileTypes: true })) {
15542
- if (!entry.isDirectory()) continue;
15543
- const path = (0, import_node_path42.join)(root, entry.name).replace(/\/$/, "");
15544
- if (known.has(path) || seen.has(path)) continue;
15545
- if (!(0, import_node_fs42.existsSync)((0, import_node_path42.join)(path, ".git"))) continue;
15546
- seen.add(path);
15547
- let mtimeMs = 0;
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(() => {
15548
15289
  try {
15549
- mtimeMs = (0, import_node_fs42.statSync)(path).mtimeMs;
15290
+ process.kill(-pid, "SIGKILL");
15550
15291
  } catch {
15551
- mtimeMs = Date.now();
15552
15292
  }
15553
- orphans.push({ path, repoPath, mtimeMs });
15554
- }
15293
+ }, 2500).unref?.();
15555
15294
  }
15556
15295
  } catch {
15296
+ try {
15297
+ child.kill("SIGTERM");
15298
+ } catch {
15299
+ }
15557
15300
  }
15558
15301
  }
15559
- return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
15560
- }
15561
- async function cleanupOrphanWorktrees(opts) {
15562
- const settings = loadAppSettings();
15563
- const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
15564
- const orphans = await findOrphanWorktrees(opts?.repoPaths);
15565
- const activeCount = listThreads().filter((t) => t.status !== "archived").length;
15566
- const budget = Math.max(0, maxCount - activeCount);
15567
- const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
15568
- const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
15569
- const removed = [];
15570
- const kept = [];
15571
- for (const orphan of orphans) {
15572
- if (keep.has(orphan.path)) {
15573
- kept.push(orphan.path);
15574
- continue;
15575
- }
15576
- if (opts?.dryRun) {
15577
- removed.push(orphan.path);
15578
- continue;
15579
- }
15580
- try {
15581
- await removeWorktree(orphan.repoPath, orphan.path);
15582
- removed.push(orphan.path);
15583
- } catch {
15584
- kept.push(orphan.path);
15585
- }
15586
- }
15587
- if (!opts?.dryRun) {
15588
- updateAdvancedSettings({
15589
- worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
15590
- });
15591
- }
15592
- return { removed, kept, orphans };
15593
- }
15594
- function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
15595
- const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
15596
- const last = settings.advanced.worktreeLastCleanupAt;
15597
- if (!last) return true;
15598
- const elapsed = Date.now() - Date.parse(last);
15599
- return elapsed >= intervalHours * 36e5;
15302
+ killListenersOnPorts(ports);
15600
15303
  }
15601
- var import_node_fs42, import_node_path42;
15602
- var init_orphan_cleanup = __esm({
15603
- "src/git/orphan-cleanup.ts"() {
15604
- "use strict";
15605
- import_node_fs42 = require("fs");
15606
- import_node_path42 = require("path");
15607
- init_worktree();
15608
- init_thread_store();
15609
- init_paths();
15610
- init_app_settings();
15611
- }
15612
- });
15613
-
15614
- // src/store/history-cleanup.ts
15615
- function stripArchivedTranscript(thread) {
15616
- writeThread(
15304
+ async function spawnWorkspaceScript(command, opts) {
15305
+ const loginEnv = await captureLoginEnv();
15306
+ const env = buildWorkspaceScriptEnv(
15617
15307
  {
15618
- ...thread,
15619
- attachments: [],
15620
- pendingTurnAttachments: [],
15621
- queueAttachments: [],
15622
- messages: [historyRetentionStubMessage(thread)]
15308
+ worktreePath: opts.worktreePath,
15309
+ repoPath: opts.repoPath,
15310
+ workspaceName: opts.workspaceName,
15311
+ defaultBranch: opts.defaultBranch,
15312
+ ports: opts.ports
15623
15313
  },
15624
- { touch: false }
15314
+ loginEnv
15625
15315
  );
15626
- }
15627
- function cleanupArchivedHistory(opts = {}) {
15628
- const settings = loadAppSettings();
15629
- if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
15630
- return { stripped: [], purged: [] };
15316
+ try {
15317
+ mergeAgentGitAuthEnv(
15318
+ env,
15319
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
15320
+ );
15321
+ } catch {
15631
15322
  }
15632
- const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
15633
- const nowMs = opts.nowMs ?? Date.now();
15634
- const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
15635
- maxCount: opts.maxCount ?? historyMaxCount(settings),
15636
- maxDays: opts.maxDays ?? historyMaxDays(settings),
15637
- nowMs
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 }
15638
15334
  });
15639
- const stripped = [];
15640
- const purged = [];
15641
- if (opts.dryRun) {
15642
- return { stripped: plan.stripIds, purged: plan.purgeIds };
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 });
15643
15350
  }
15644
- for (const id of plan.stripIds) {
15645
- const thread = readThread(id);
15646
- if (!thread || thread.status !== "archived") continue;
15647
- try {
15648
- stripArchivedTranscript(thread);
15649
- stripped.push(id);
15650
- } catch {
15651
- }
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`);
15652
15395
  }
15653
- for (const id of plan.purgeIds) {
15654
- const thread = readThread(id);
15655
- if (!thread || thread.status !== "archived") continue;
15656
- try {
15657
- deleteThreadRecord(id);
15658
- purged.push(id);
15659
- } catch {
15660
- }
15396
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
15397
+ if (!setup.ran) {
15398
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
15661
15399
  }
15662
- if (stripped.length > 0 || purged.length > 0) {
15663
- updateAdvancedSettings({
15664
- historyLastCleanupAt: new Date(nowMs).toISOString()
15665
- });
15400
+ if (!setup.ran) {
15401
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
15666
15402
  }
15667
- return { stripped, purged };
15403
+ return setup;
15668
15404
  }
15669
- function shouldRunHistoryCleanup(settings = loadAppSettings()) {
15670
- if (!autoCleanupHistoryEnabled(settings)) return false;
15671
- const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
15672
- const last = settings.advanced.historyLastCleanupAt;
15673
- if (!last) return true;
15674
- const elapsed = Date.now() - Date.parse(last);
15675
- return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
15676
- }
15677
- var init_history_cleanup = __esm({
15678
- "src/store/history-cleanup.ts"() {
15679
- "use strict";
15680
- init_app_settings();
15681
- init_history_retention();
15682
- init_thread_store();
15683
- }
15684
- });
15685
-
15686
- // src/git/apply-into-main.ts
15687
- async function applyThreadIntoMain(thread, opts) {
15688
- const method = opts?.method ?? "merge";
15689
- const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
15690
- const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
15691
- cwd: thread.repoPath,
15692
- reject: false
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
15693
15412
  });
15694
- if (status.stdout.trim()) {
15695
- throw new Error(
15696
- `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
15697
- );
15698
- }
15699
- const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
15700
- cwd: thread.repoPath,
15701
- reject: false
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");
15702
15422
  });
15703
- if (checkout.exitCode !== 0) {
15704
- throw new Error(
15705
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
15706
- );
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;
15707
15434
  }
15708
- if (method === "cherry-pick") {
15709
- const log = await (0, import_execa5.execa)(
15710
- "git",
15711
- ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
15712
- { cwd: thread.repoPath, reject: false }
15713
- );
15714
- const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
15715
- if (!shas.length) {
15716
- return {
15717
- applied: false,
15718
- method,
15719
- targetBranch: target,
15720
- message: `No commits on ${thread.branchName} not already in ${target}`
15721
- };
15722
- }
15723
- for (const sha of shas) {
15724
- const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
15725
- cwd: thread.repoPath,
15726
- reject: false
15727
- });
15728
- if (cp.exitCode !== 0) {
15729
- await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
15730
- cwd: thread.repoPath,
15731
- reject: false
15732
- });
15733
- throw new Error(
15734
- `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
15735
- );
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;
15444
+ const release = () => new Promise((res) => {
15445
+ if (released) {
15446
+ res();
15447
+ return;
15736
15448
  }
15737
- }
15738
- return {
15739
- applied: true,
15740
- method,
15741
- targetBranch: target,
15742
- message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
15743
- };
15744
- }
15745
- const merge = await (0, import_execa5.execa)(
15746
- "git",
15747
- ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
15748
- { cwd: thread.repoPath, reject: false }
15749
- );
15750
- if (merge.exitCode !== 0) {
15751
- await (0, import_execa5.execa)("git", ["merge", "--abort"], {
15752
- cwd: thread.repoPath,
15753
- reject: false
15449
+ released = true;
15450
+ server.close(() => res());
15754
15451
  });
15755
- throw new Error(
15756
- `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
15757
- );
15758
- }
15759
- return {
15760
- applied: true,
15761
- method: "merge",
15762
- targetBranch: target,
15763
- message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
15764
- };
15452
+ server.listen(0, "127.0.0.1", () => {
15453
+ const addr = server.address();
15454
+ if (!addr || typeof addr === "string") {
15455
+ void release().then(() => reject(new Error("Failed to allocate port")));
15456
+ return;
15457
+ }
15458
+ resolve2({ port: addr.port, release });
15459
+ });
15460
+ server.on("error", reject);
15461
+ });
15765
15462
  }
15766
- var import_execa5;
15767
- var init_apply_into_main = __esm({
15768
- "src/git/apply-into-main.ts"() {
15769
- "use strict";
15770
- import_execa5 = require("execa");
15771
- init_worktree();
15772
- }
15773
- });
15774
-
15775
- // src/git/clone-repo.ts
15776
- async function cloneRepoIntoSideboard(opts) {
15777
- const url = opts.url.trim();
15778
- if (!url) throw new Error("Clone URL is required");
15779
- let name = opts.name?.trim();
15780
- if (!name) {
15781
- const leaf = (0, import_node_path43.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
15782
- name = leaf || "repo";
15783
- }
15784
- name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
15785
- const dest = (0, import_node_path43.join)(sideboardReposDir(), name);
15786
- if ((0, import_node_fs43.existsSync)(dest)) {
15787
- const repoPath2 = await resolveRepoRoot(dest);
15788
- const workspace2 = await ensureWorkspace(repoPath2);
15789
- return { repoPath: repoPath2, workspace: workspace2 };
15790
- }
15791
- const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
15792
- if (result.exitCode !== 0) {
15793
- throw new Error(
15794
- `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
15795
- );
15796
- }
15797
- const repoPath = await resolveRepoRoot(dest);
15798
- const workspace = await ensureWorkspace(repoPath);
15799
- return { repoPath, workspace };
15463
+ async function tryReservePort(port) {
15464
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
15465
+ return new Promise((resolve2) => {
15466
+ const server = (0, import_node_net.createServer)();
15467
+ let released = false;
15468
+ const release = () => new Promise((res) => {
15469
+ if (released) {
15470
+ res();
15471
+ return;
15472
+ }
15473
+ released = true;
15474
+ server.close(() => res());
15475
+ });
15476
+ server.once("error", () => {
15477
+ try {
15478
+ server.close();
15479
+ } catch {
15480
+ }
15481
+ resolve2(null);
15482
+ });
15483
+ server.listen(port, "127.0.0.1", () => {
15484
+ resolve2({ port, release });
15485
+ });
15486
+ });
15800
15487
  }
15801
- var import_node_fs43, import_node_path43, import_execa6;
15802
- var init_clone_repo = __esm({
15803
- "src/git/clone-repo.ts"() {
15804
- "use strict";
15805
- import_node_fs43 = require("fs");
15806
- import_node_path43 = require("path");
15807
- import_execa6 = require("execa");
15808
- init_paths();
15809
- init_workspaces();
15810
- init_worktree();
15488
+ function normalizePreferredPorts(ports, size) {
15489
+ if (!ports?.length) return [];
15490
+ const seen = /* @__PURE__ */ new Set();
15491
+ const out = [];
15492
+ for (const p of ports) {
15493
+ if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
15494
+ seen.add(p);
15495
+ out.push(p);
15496
+ if (out.length >= size) break;
15811
15497
  }
15812
- });
15813
-
15814
- // src/types/thread.ts
15815
- function isAdoptableRunScriptRequest(req) {
15816
- return Boolean(req && !req.fulfilledAt && !req.error);
15498
+ return out;
15817
15499
  }
15818
- var init_thread = __esm({
15819
- "src/types/thread.ts"() {
15820
- "use strict";
15821
- }
15822
- });
15823
-
15824
- // src/store/desktop-host.ts
15825
- function desktopHostPidPath() {
15826
- return (0, import_node_path44.join)(appDataDir(), "desktop-host.pid");
15500
+ function delay(ms) {
15501
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
15827
15502
  }
15828
- function readDesktopHostPid() {
15503
+ async function fillPortRange(held, size) {
15504
+ if (held.length >= size) return held;
15505
+ const base = held[0]?.port;
15506
+ const taken = new Set(held.map((h) => h.port));
15829
15507
  try {
15830
- const pid = Number.parseInt((0, import_node_fs44.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
15831
- if (!Number.isFinite(pid) || pid <= 0) return null;
15832
- return pid;
15833
- } catch {
15834
- return null;
15508
+ let offset = 0;
15509
+ while (held.length < size) {
15510
+ const candidate = base != null ? base + offset : null;
15511
+ offset += 1;
15512
+ if (candidate != null && taken.has(candidate)) continue;
15513
+ const next = candidate != null ? await tryReservePort(candidate) : null;
15514
+ if (next) {
15515
+ held.push(next);
15516
+ taken.add(next.port);
15517
+ } else {
15518
+ const ephemeral = await reservePort();
15519
+ held.push(ephemeral);
15520
+ taken.add(ephemeral.port);
15521
+ }
15522
+ }
15523
+ return held;
15524
+ } catch (err) {
15525
+ await Promise.all(held.map((h) => h.release()));
15526
+ throw err;
15835
15527
  }
15836
15528
  }
15837
- function pidAlive(pid) {
15838
- try {
15839
- process.kill(pid, 0);
15840
- return true;
15841
- } catch {
15842
- return false;
15529
+ async function tryReserveAvailablePorts(ports) {
15530
+ const held = [];
15531
+ for (const port of ports) {
15532
+ const next = await tryReservePort(port);
15533
+ if (next) held.push(next);
15843
15534
  }
15535
+ return held;
15844
15536
  }
15845
- function isDesktopHostAlive() {
15846
- const pid = readDesktopHostPid();
15847
- return pid != null && pidAlive(pid);
15848
- }
15849
- function isThisProcessDesktopHost() {
15850
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
15851
- }
15852
- function thisProcessShouldDrainAgentQueues() {
15853
- if (isThisProcessDesktopHost()) return true;
15854
- return !isDesktopHostAlive();
15855
- }
15856
- var import_node_fs44, import_node_path44;
15857
- var init_desktop_host = __esm({
15858
- "src/store/desktop-host.ts"() {
15859
- "use strict";
15860
- import_node_fs44 = require("fs");
15861
- import_node_path44 = require("path");
15862
- init_paths();
15537
+ async function reclaimPreferredPorts(ports, worktreePath) {
15538
+ const primary = ports[0];
15539
+ if (primary == null) return null;
15540
+ const takePrimary = async () => {
15541
+ const held2 = await tryReserveAvailablePorts(ports);
15542
+ if (held2.some((h) => h.port === primary)) return held2;
15543
+ await Promise.all(held2.map((h) => h.release()));
15544
+ return null;
15545
+ };
15546
+ let held = await takePrimary();
15547
+ if (held) return held;
15548
+ if (worktreePath) {
15549
+ killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
15550
+ } else {
15551
+ killListenersOnPorts(ports);
15863
15552
  }
15864
- });
15865
-
15866
- // src/orchestrator/child-halt.ts
15867
- function isIncompleteChildStatus(status) {
15868
- return HALT_STATUSES.has(status);
15869
- }
15870
- function childHaltNotice(child, status) {
15871
- const title = child.title?.trim() || "Untitled";
15872
- const link = `[${title}](sideboard://thread/${child.id})`;
15873
- const why = child.lastError?.trim();
15874
- const extra = why ? ` lastError: ${why}` : "";
15875
- return [
15876
- `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
15877
- "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."
15878
- ].join("\n");
15879
- }
15880
- function shouldNotifyParentAfterTurnError(opts) {
15881
- return !opts.quotaFailoverHandled && !opts.crashContinued;
15882
- }
15883
- function shouldNotifyParentOfChildHalt(opts) {
15884
- if (!isIncompleteChildStatus(opts.status)) return false;
15885
- if (!opts.child.parentThreadId) return false;
15886
- if (!opts.parent || opts.parent.status === "archived") return false;
15887
- if (opts.parent.id === opts.child.id) return false;
15888
- return isOrchestratorThread(opts.parent);
15553
+ for (let i = 0; i < RECLAIM_TRIES; i++) {
15554
+ await delay(RECLAIM_RETRY_DELAY_MS);
15555
+ held = await takePrimary();
15556
+ if (held) return held;
15557
+ if (i === 2 && worktreePath) {
15558
+ killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
15559
+ }
15560
+ }
15561
+ return null;
15889
15562
  }
15890
- function noticeKey(childId, status) {
15891
- return `${childId}:${status}`;
15563
+ async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
15564
+ const preferred = normalizePreferredPorts(opts?.preferred, size);
15565
+ if (preferred.length) {
15566
+ const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
15567
+ if (reused) return fillPortRange(reused, size);
15568
+ }
15569
+ const held = [await reservePort()];
15570
+ return fillPortRange(held, size);
15892
15571
  }
15893
- function notifyParentOfChildHalt(child, status, send) {
15894
- const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
15895
- if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
15896
- const key = noticeKey(child.id, status);
15897
- if (notified.has(key)) return false;
15898
- notified.add(key);
15899
- const parentId = parent.id;
15900
- void send(parentId, childHaltNotice(child, status)).catch(() => {
15901
- notified.delete(key);
15572
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
15573
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
15574
+ if (!script) return null;
15575
+ const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
15576
+ const held = await reservePortRange(PORT_RANGE_SIZE, {
15577
+ preferred: preferred ?? void 0,
15578
+ worktreePath
15902
15579
  });
15903
- return true;
15580
+ const ports = held.map((h) => h.port);
15581
+ saveWorktreeRunPorts(worktreePath, script.name, ports);
15582
+ try {
15583
+ const handle = await spawnWorkspaceScript(script.command, {
15584
+ worktreePath,
15585
+ repoPath,
15586
+ ports,
15587
+ heldPorts: held,
15588
+ defaultBranch: opts?.defaultBranch,
15589
+ onLine
15590
+ });
15591
+ return {
15592
+ pid: handle.pid,
15593
+ port: ports[0],
15594
+ ports,
15595
+ scriptName: script.name,
15596
+ kill: handle.kill,
15597
+ done: handle.done
15598
+ };
15599
+ } catch (err) {
15600
+ await Promise.all(held.map((h) => h.release()));
15601
+ throw err;
15602
+ }
15904
15603
  }
15905
- var HALT_STATUSES, notified;
15906
- var init_child_halt = __esm({
15907
- "src/orchestrator/child-halt.ts"() {
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;
15605
+ var init_conductor = __esm({
15606
+ "src/hook/conductor.ts"() {
15908
15607
  "use strict";
15909
- init_global_workspace();
15910
- init_thread_store();
15911
- HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
15912
- notified = /* @__PURE__ */ new Set();
15608
+ import_node_fs40 = require("fs");
15609
+ import_node_child_process4 = require("child_process");
15610
+ import_node_net = require("net");
15611
+ import_node_path40 = require("path");
15612
+ import_execa4 = require("execa");
15613
+ import_node_readline3 = require("readline");
15614
+ init_settings();
15615
+ init_nested_electron_env();
15616
+ init_worktree_pkg_cache();
15617
+ init_convention_setup();
15618
+ init_cursor_worktrees();
15619
+ init_git_auth_mode();
15620
+ init_request_review();
15621
+ init_worktree_exclude();
15622
+ init_worktree_run_ports();
15623
+ init_wait_for_job();
15624
+ init_worktree_run_ports();
15625
+ init_nested_electron_env();
15626
+ PORT_RANGE_SIZE = 10;
15627
+ RECLAIM_RETRY_DELAY_MS = 40;
15628
+ RECLAIM_TRIES = 8;
15629
+ cachedLoginEnv = null;
15913
15630
  }
15914
15631
  });
15915
15632
 
15916
- // src/mcp/wait-for-turn.ts
15917
- function mcpWaitForTurnTimeoutMs(requested) {
15918
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
15919
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
15920
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
15633
+ // src/store/setup-log.ts
15634
+ function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
15635
+ const next = prev ? `${prev}
15636
+ ${line}` : line;
15637
+ if (next.length <= max) return next;
15638
+ const cut = next.slice(next.length - max);
15639
+ const nl = cut.indexOf("\n");
15640
+ return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
15921
15641
  }
15922
- function mcpWaitTaskHint(taskState, status) {
15923
- switch (taskState) {
15924
- case "submitted":
15925
- return MCP_WAIT_QUEUED_HINT;
15926
- case "working":
15927
- return MCP_WAIT_STILL_RUNNING_HINT;
15928
- case "input-required":
15929
- return MCP_WAIT_INPUT_REQUIRED_HINT;
15930
- case "canceled":
15931
- return MCP_WAIT_STOPPED_HINT;
15932
- case "failed":
15933
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
15934
- default:
15935
- return void 0;
15936
- }
15642
+ function fromSnapshot(snap) {
15643
+ return {
15644
+ chunks: snap.output ? [snap.output] : [],
15645
+ length: snap.output.length,
15646
+ running: snap.running,
15647
+ exitCode: snap.exitCode,
15648
+ source: snap.source
15649
+ };
15937
15650
  }
15938
- 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;
15939
- var init_wait_for_turn = __esm({
15940
- "src/mcp/wait-for-turn.ts"() {
15941
- "use strict";
15942
- MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
15943
- 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.";
15944
- 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.";
15945
- 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.";
15946
- MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
15947
- 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.";
15948
- 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.";
15651
+ function materialize(mem) {
15652
+ let output = mem.chunks.join("\n");
15653
+ if (output.length > MAX_SETUP_LOG_CHARS) {
15654
+ output = appendSetupOutput("", output);
15655
+ mem.chunks = [output];
15656
+ mem.length = output.length;
15949
15657
  }
15950
- });
15951
-
15952
- // src/mcp/wait-for-job.ts
15953
- function mcpWaitForJobTimeoutMs(requested) {
15954
- return mcpWaitForTurnTimeoutMs(requested);
15658
+ return {
15659
+ output,
15660
+ running: mem.running,
15661
+ exitCode: mem.exitCode,
15662
+ source: mem.source
15663
+ };
15955
15664
  }
15956
- function sanitizeDetachedJobId(id) {
15957
- const s = id.trim();
15958
- if (!JOB_ID_RE.test(s)) {
15959
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
15960
- }
15961
- return s;
15665
+ function setupLogPath(threadId) {
15666
+ return (0, import_node_path41.join)(threadsDir(), `${threadId}.setup.log.json`);
15962
15667
  }
15963
- function jobAlive(pid) {
15964
- if (!Number.isInteger(pid) || pid <= 0) return false;
15668
+ function setupLogKeyForWorktree(worktreePath) {
15669
+ const norm = normalizeWorktreePath(worktreePath);
15670
+ return `wt-${(0, import_node_crypto8.createHash)("sha1").update(norm).digest("hex").slice(0, 16)}`;
15671
+ }
15672
+ function emptySetupLog() {
15673
+ return { ...EMPTY };
15674
+ }
15675
+ function persistNow(threadId) {
15676
+ const timer = persistTimers.get(threadId);
15677
+ if (timer) {
15678
+ clearTimeout(timer);
15679
+ persistTimers.delete(threadId);
15680
+ }
15681
+ const mem = memory.get(threadId);
15682
+ if (!mem) return;
15965
15683
  try {
15966
- process.kill(pid, 0);
15967
- return true;
15684
+ (0, import_node_fs41.writeFileSync)(
15685
+ setupLogPath(threadId),
15686
+ `${JSON.stringify(materialize(mem))}
15687
+ `,
15688
+ "utf8"
15689
+ );
15968
15690
  } catch {
15969
- return false;
15970
15691
  }
15971
15692
  }
15972
- function processGroupAlive(pgid) {
15973
- if (process.platform === "win32") return false;
15974
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
15693
+ function schedulePersist(threadId) {
15694
+ if (persistTimers.has(threadId)) return;
15695
+ persistTimers.set(
15696
+ threadId,
15697
+ setTimeout(() => {
15698
+ persistTimers.delete(threadId);
15699
+ persistNow(threadId);
15700
+ }, PERSIST_DEBOUNCE_MS)
15701
+ );
15702
+ }
15703
+ function loadFromDisk(threadId) {
15704
+ const path = setupLogPath(threadId);
15705
+ if (!(0, import_node_fs41.existsSync)(path)) return null;
15975
15706
  try {
15976
- process.kill(-pgid, 0);
15977
- return true;
15978
- } catch (err) {
15979
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
15707
+ const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
15708
+ return {
15709
+ output: typeof raw.output === "string" ? raw.output : "",
15710
+ running: Boolean(raw.running),
15711
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15712
+ source: typeof raw.source === "string" ? raw.source : null
15713
+ };
15714
+ } catch {
15715
+ return null;
15980
15716
  }
15981
15717
  }
15982
- function jobTreeAlive(pid) {
15983
- if (pid == null) return false;
15984
- return jobAlive(pid) || processGroupAlive(pid);
15718
+ function memFor(threadId) {
15719
+ const cached = memory.get(threadId);
15720
+ if (cached) return cached;
15721
+ const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
15722
+ memory.set(threadId, mem);
15723
+ return mem;
15985
15724
  }
15986
- function readIntFile(file) {
15987
- if (!(0, import_node_fs45.existsSync)(file)) return null;
15988
- const n = Number.parseInt((0, import_node_fs45.readFileSync)(file, "utf8").trim(), 10);
15989
- return Number.isInteger(n) ? n : null;
15725
+ function readSetupLog(threadId) {
15726
+ const cached = memory.get(threadId);
15727
+ if (cached) return materialize(cached);
15728
+ const disk = loadFromDisk(threadId);
15729
+ if (disk) {
15730
+ memory.set(threadId, fromSnapshot(disk));
15731
+ return disk;
15732
+ }
15733
+ return emptySetupLog();
15990
15734
  }
15991
- function jobDir(root, id, legacy = false) {
15992
- return (0, import_node_path45.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
15735
+ function beginSetupLog(threadId) {
15736
+ const mem = {
15737
+ chunks: [],
15738
+ length: 0,
15739
+ running: true,
15740
+ exitCode: null,
15741
+ source: null
15742
+ };
15743
+ memory.set(threadId, mem);
15744
+ persistNow(threadId);
15745
+ return materialize(mem);
15993
15746
  }
15994
- function resolveJobDir(root, id) {
15995
- const modern = jobDir(root, id, false);
15996
- if ((0, import_node_fs45.existsSync)(modern)) return modern;
15997
- const legacy = jobDir(root, id, true);
15998
- if ((0, import_node_fs45.existsSync)(legacy)) return legacy;
15999
- return modern;
15747
+ function appendSetupLog(threadId, line) {
15748
+ const mem = memFor(threadId);
15749
+ mem.chunks.push(line);
15750
+ mem.length += line.length + 1;
15751
+ mem.running = true;
15752
+ if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
15753
+ schedulePersist(threadId);
16000
15754
  }
16001
- function listJobIdsIn(root, rel) {
16002
- const dir = (0, import_node_path45.join)(root, rel);
16003
- if (!(0, import_node_fs45.existsSync)(dir)) return [];
16004
- return (0, import_node_fs45.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
15755
+ function finishSetupLog(threadId, exitCode, source) {
15756
+ const mem = memFor(threadId);
15757
+ mem.running = false;
15758
+ mem.exitCode = exitCode;
15759
+ mem.source = source ?? mem.source;
15760
+ persistNow(threadId);
15761
+ return materialize(mem);
16005
15762
  }
16006
- function listRunningDetachedJobs(worktreePath) {
16007
- const root = worktreePath.trim();
16008
- if (!root) return [];
16009
- const ids = /* @__PURE__ */ new Set([
16010
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
16011
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
16012
- ]);
16013
- const running = [];
16014
- for (const id of ids) {
16015
- const dir = resolveJobDir(root, id);
16016
- const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
16017
- if (jobTreeAlive(pid)) running.push(id);
15763
+ var import_node_crypto8, import_node_fs41, import_node_path41, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
15764
+ var init_setup_log = __esm({
15765
+ "src/store/setup-log.ts"() {
15766
+ "use strict";
15767
+ import_node_crypto8 = require("crypto");
15768
+ import_node_fs41 = require("fs");
15769
+ import_node_path41 = require("path");
15770
+ init_worktree_labels();
15771
+ init_paths();
15772
+ EMPTY = {
15773
+ output: "",
15774
+ running: false,
15775
+ exitCode: null,
15776
+ source: null
15777
+ };
15778
+ MAX_SETUP_LOG_CHARS = 256e3;
15779
+ memory = /* @__PURE__ */ new Map();
15780
+ persistTimers = /* @__PURE__ */ new Map();
15781
+ PERSIST_DEBOUNCE_MS = 500;
16018
15782
  }
16019
- return running.sort();
15783
+ });
15784
+
15785
+ // src/store/run-log.ts
15786
+ function fromSnapshot2(snap) {
15787
+ return {
15788
+ chunks: snap.output ? [snap.output] : [],
15789
+ length: snap.output.length,
15790
+ running: snap.running,
15791
+ exitCode: snap.exitCode,
15792
+ scriptName: snap.scriptName
15793
+ };
16020
15794
  }
16021
- function looksLikeDeferredDonePromise(text7) {
16022
- const t = (text7 ?? "").trim();
16023
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
16024
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
16025
- if (!unquoted) return false;
16026
- 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);
15795
+ function materialize2(mem) {
15796
+ let output = mem.chunks.join("\n");
15797
+ if (output.length > MAX_RUN_LOG_CHARS) {
15798
+ output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
15799
+ mem.chunks = [output];
15800
+ mem.length = output.length;
15801
+ }
15802
+ return {
15803
+ output,
15804
+ running: mem.running,
15805
+ exitCode: mem.exitCode,
15806
+ scriptName: mem.scriptName
15807
+ };
16027
15808
  }
16028
- function isJobContinuePrompt(text7) {
16029
- if (typeof text7 !== "string") return false;
16030
- const t = text7.trim();
16031
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
15809
+ function safeScriptToken(scriptName) {
15810
+ const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
15811
+ return token || "dev";
16032
15812
  }
16033
- function collapseGhRunWatchLog(text7) {
16034
- if (!text7.includes(GH_WATCH_REFRESH)) return text7;
16035
- const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
16036
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
16037
- if (watchFrames.length <= 1) return text7;
16038
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
16039
- const last = watchFrames[watchFrames.length - 1] ?? "";
16040
- const omitted = watchFrames.length - 1;
16041
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
16042
- `;
16043
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
15813
+ function runLogPath(key) {
15814
+ return (0, import_node_path42.join)(threadsDir(), `${key}.run.log.json`);
16044
15815
  }
16045
- function capJobLogDelta(text7) {
16046
- const collapsed = collapseGhRunWatchLog(text7);
16047
- const lines = collapsed.split("\n");
16048
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
16049
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
16050
- if (out.length > MAX_JOB_DELTA_CHARS) {
16051
- out = `\u2026(truncated ${out.length} chars)
16052
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
16053
- }
16054
- return out;
15816
+ function runLogKeyForWorktree(worktreePath, scriptName) {
15817
+ const norm = normalizeWorktreePath(worktreePath);
15818
+ const hash = (0, import_node_crypto9.createHash)("sha1").update(norm).digest("hex").slice(0, 16);
15819
+ return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
16055
15820
  }
16056
- function formatJobStillRunningContinuePrompt(jobIds) {
16057
- const ids = jobIds.join(", ");
16058
- return [
16059
- `Detached job still running: ${ids}.`,
16060
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
16061
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
16062
- "Then report the result. Do not tell the user you will let them know later."
16063
- ].join(" ");
15821
+ function emptyRunLog() {
15822
+ return { ...EMPTY2 };
16064
15823
  }
16065
- function formatDeferredDoneContinuePrompt() {
16066
- return [
16067
- "You ended the turn after promising to report later, but no detached job is running.",
16068
- "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).",
16069
- "Do not say you will let the user know later."
16070
- ].join(" ");
15824
+ function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
15825
+ const max = Math.max(1, tailChars);
15826
+ const outputChars = output.length;
15827
+ if (outputChars <= max) return { output, truncated: false, outputChars };
15828
+ const cut = output.slice(outputChars - max);
15829
+ const nl = cut.indexOf("\n");
15830
+ const text7 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
15831
+ return { output: text7, truncated: true, outputChars };
16071
15832
  }
16072
- function turnWatchedDetachedJob(parts) {
16073
- return parts.some((p) => {
16074
- if (p.type !== "tool") return false;
16075
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
16076
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
16077
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
16078
- });
15833
+ function persistNow2(key) {
15834
+ const timer = persistTimers2.get(key);
15835
+ if (timer) {
15836
+ clearTimeout(timer);
15837
+ persistTimers2.delete(key);
15838
+ }
15839
+ const mem = memory2.get(key);
15840
+ if (!mem) return;
15841
+ try {
15842
+ (0, import_node_fs42.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15843
+ `, "utf8");
15844
+ } catch {
15845
+ }
16079
15846
  }
16080
- function planJobContinue(opts) {
16081
- if (opts.isOrchestrator) return { action: "none" };
16082
- if (opts.agent === "brightsy") return { action: "none" };
16083
- if (opts.queueLength > 0) return { action: "none" };
16084
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
16085
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
16086
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
15847
+ function schedulePersist2(key) {
15848
+ if (persistTimers2.has(key)) return;
15849
+ persistTimers2.set(
15850
+ key,
15851
+ setTimeout(() => {
15852
+ persistTimers2.delete(key);
15853
+ persistNow2(key);
15854
+ }, PERSIST_DEBOUNCE_MS2)
15855
+ );
15856
+ }
15857
+ function loadFromDisk2(key) {
15858
+ const path = runLogPath(key);
15859
+ if (!(0, import_node_fs42.existsSync)(path)) return null;
15860
+ try {
15861
+ const raw = JSON.parse((0, import_node_fs42.readFileSync)(path, "utf8"));
16087
15862
  return {
16088
- action: "wait",
16089
- jobIds: opts.runningJobIds,
16090
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
15863
+ output: typeof raw.output === "string" ? raw.output : "",
15864
+ running: Boolean(raw.running),
15865
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15866
+ scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
16091
15867
  };
15868
+ } catch {
15869
+ return null;
16092
15870
  }
16093
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
16094
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
16095
- }
16096
- return { action: "none" };
16097
15871
  }
16098
- function tailProgress(logFile, maxLines = 12) {
16099
- if (!(0, import_node_fs45.existsSync)(logFile)) return "(no log yet)";
16100
- const lines = (0, import_node_fs45.readFileSync)(logFile, "utf8").split("\n");
16101
- return lines.slice(-maxLines).join("\n");
15872
+ function memFor2(key) {
15873
+ const cached = memory2.get(key);
15874
+ if (cached) return cached;
15875
+ const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
15876
+ memory2.set(key, mem);
15877
+ return mem;
16102
15878
  }
16103
- function lastProgressLine(progress) {
16104
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
16105
- return last ? last.slice(0, 72) : void 0;
15879
+ function readRunLog(key) {
15880
+ const cached = memory2.get(key);
15881
+ if (cached) return materialize2(cached);
15882
+ const disk = loadFromDisk2(key);
15883
+ if (disk) {
15884
+ memory2.set(key, fromSnapshot2(disk));
15885
+ return disk;
15886
+ }
15887
+ return emptyRunLog();
16106
15888
  }
16107
- function readLogLines(file) {
16108
- if (!(0, import_node_fs45.existsSync)(file)) return [];
16109
- const lines = (0, import_node_fs45.readFileSync)(file, "utf8").split("\n");
16110
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
16111
- return lines;
15889
+ function beginRunLog(key, scriptName) {
15890
+ const mem = {
15891
+ chunks: [],
15892
+ length: 0,
15893
+ running: true,
15894
+ exitCode: null,
15895
+ scriptName
15896
+ };
15897
+ memory2.set(key, mem);
15898
+ persistNow2(key);
15899
+ return materialize2(mem);
16112
15900
  }
16113
- function takeDelta(logFile, cursorFile) {
16114
- const lines = readLogLines(logFile);
16115
- const cursor = readIntFile(cursorFile) ?? 0;
16116
- const start = Math.min(Math.max(0, cursor), lines.length);
16117
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
15901
+ function appendRunLog(key, line) {
15902
+ const mem = memFor2(key);
15903
+ mem.chunks.push(line);
15904
+ mem.length += line.length + 1;
15905
+ mem.running = true;
15906
+ if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
15907
+ schedulePersist2(key);
16118
15908
  }
16119
- function snapshotJob(dir) {
16120
- const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
16121
- const running = jobTreeAlive(pid);
16122
- return {
16123
- pid,
16124
- running,
16125
- exitCode: readIntFile((0, import_node_path45.join)(dir, "exit")),
16126
- log: (0, import_node_path45.join)(dir, "log"),
16127
- cursor: (0, import_node_path45.join)(dir, "present.cursor"),
16128
- progress: tailProgress((0, import_node_path45.join)(dir, "log"))
16129
- };
15909
+ function finishRunLog(key, exitCode) {
15910
+ const mem = memFor2(key);
15911
+ mem.running = false;
15912
+ mem.exitCode = exitCode;
15913
+ persistNow2(key);
15914
+ return materialize2(mem);
16130
15915
  }
16131
- function toResult(id, snap, extra) {
16132
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
16133
- const ok = !snap.running && snap.exitCode === 0;
16134
- const stillRunning = snap.running && !ok;
16135
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
16136
- try {
16137
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(snap.cursor), { recursive: true });
16138
- (0, import_node_fs45.writeFileSync)(snap.cursor, `${nextCursor}
16139
- `);
16140
- } catch {
15916
+ function flushRunLog(key) {
15917
+ persistNow2(key);
15918
+ }
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;
15920
+ var init_run_log = __esm({
15921
+ "src/store/run-log.ts"() {
15922
+ "use strict";
15923
+ import_node_crypto9 = require("crypto");
15924
+ import_node_fs42 = require("fs");
15925
+ import_node_path42 = require("path");
15926
+ init_worktree_labels();
15927
+ init_setup_log();
15928
+ init_paths();
15929
+ EMPTY2 = {
15930
+ output: "",
15931
+ running: false,
15932
+ exitCode: null,
15933
+ scriptName: null
15934
+ };
15935
+ MAX_RUN_LOG_CHARS = 256e3;
15936
+ DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
15937
+ memory2 = /* @__PURE__ */ new Map();
15938
+ persistTimers2 = /* @__PURE__ */ new Map();
15939
+ PERSIST_DEBOUNCE_MS2 = 500;
16141
15940
  }
16142
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
16143
- const progress = extra?.progress ?? snap.progress;
16144
- return {
16145
- stillRunning,
16146
- ok,
16147
- failed: Boolean(failed && !stillRunning && !ok),
16148
- status,
16149
- id,
16150
- pid: snap.pid,
16151
- exitCode: snap.exitCode ?? void 0,
16152
- phase: lastProgressLine(progress),
16153
- delta,
16154
- progress,
16155
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
16156
- };
15941
+ });
15942
+
15943
+ // src/git/orphan-cleanup.ts
15944
+ function isSideboardWorktreePath(path) {
15945
+ return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
16157
15946
  }
16158
- async function sleepMs(ms) {
16159
- await new Promise((resolve2) => setTimeout(resolve2, ms));
15947
+ async function findOrphanWorktrees(repoPaths) {
15948
+ const threads = listThreads({ includeArchived: true });
15949
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
15950
+ const repos = new Set(
15951
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
15952
+ );
15953
+ const homeRoot = sideboardWorkspacesDir();
15954
+ if ((0, import_node_fs43.existsSync)(homeRoot)) {
15955
+ try {
15956
+ for (const entry of (0, import_node_fs43.readdirSync)(homeRoot, { withFileTypes: true })) {
15957
+ if (!entry.isDirectory()) continue;
15958
+ void entry;
15959
+ }
15960
+ } catch {
15961
+ }
15962
+ }
15963
+ const orphans = [];
15964
+ const seen = /* @__PURE__ */ new Set();
15965
+ for (const repoPath of repos) {
15966
+ if (!repoPath || !(0, import_node_fs43.existsSync)(repoPath)) continue;
15967
+ try {
15968
+ const wts = await listWorktrees(repoPath);
15969
+ for (const wt of wts) {
15970
+ const path = wt.path.replace(/\/$/, "");
15971
+ if (!isSideboardWorktreePath(path)) continue;
15972
+ if (known.has(path)) continue;
15973
+ if (seen.has(path)) continue;
15974
+ seen.add(path);
15975
+ let mtimeMs = 0;
15976
+ try {
15977
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
15978
+ } catch {
15979
+ mtimeMs = 0;
15980
+ }
15981
+ orphans.push({ path, repoPath, mtimeMs });
15982
+ }
15983
+ } catch {
15984
+ }
15985
+ try {
15986
+ const root = worktreesRoot(repoPath);
15987
+ if ((0, import_node_fs43.existsSync)(root)) {
15988
+ for (const entry of (0, import_node_fs43.readdirSync)(root, { withFileTypes: true })) {
15989
+ if (!entry.isDirectory()) continue;
15990
+ const path = (0, import_node_path43.join)(root, entry.name).replace(/\/$/, "");
15991
+ if (known.has(path) || seen.has(path)) continue;
15992
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path43.join)(path, ".git"))) continue;
15993
+ seen.add(path);
15994
+ let mtimeMs = 0;
15995
+ try {
15996
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
15997
+ } catch {
15998
+ mtimeMs = Date.now();
15999
+ }
16000
+ orphans.push({ path, repoPath, mtimeMs });
16001
+ }
16002
+ }
16003
+ } catch {
16004
+ }
16005
+ }
16006
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
16160
16007
  }
16161
- async function waitForDetachedJob(cwd, id, opts) {
16162
- const jobId = sanitizeDetachedJobId(id);
16163
- const root = cwd.trim() || process.cwd();
16164
- const dir = resolveJobDir(root, jobId);
16165
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
16166
- const sleep = opts?.sleep ?? sleepMs;
16167
- if (!(0, import_node_fs45.existsSync)(dir)) {
16008
+ async function cleanupOrphanWorktrees(opts) {
16009
+ const settings = loadAppSettings();
16010
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
16011
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
16012
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
16013
+ const budget = Math.max(0, maxCount - activeCount);
16014
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
16015
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
16016
+ const removed = [];
16017
+ const kept = [];
16018
+ for (const orphan of orphans) {
16019
+ if (keep.has(orphan.path)) {
16020
+ kept.push(orphan.path);
16021
+ continue;
16022
+ }
16023
+ if (opts?.dryRun) {
16024
+ removed.push(orphan.path);
16025
+ continue;
16026
+ }
16027
+ try {
16028
+ await removeWorktree(orphan.repoPath, orphan.path);
16029
+ removed.push(orphan.path);
16030
+ } catch {
16031
+ kept.push(orphan.path);
16032
+ }
16033
+ }
16034
+ if (!opts?.dryRun) {
16035
+ updateAdvancedSettings({
16036
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
16037
+ });
16038
+ }
16039
+ return { removed, kept, orphans };
16040
+ }
16041
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
16042
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
16043
+ const last = settings.advanced.worktreeLastCleanupAt;
16044
+ if (!last) return true;
16045
+ const elapsed = Date.now() - Date.parse(last);
16046
+ return elapsed >= intervalHours * 36e5;
16047
+ }
16048
+ var import_node_fs43, import_node_path43;
16049
+ var init_orphan_cleanup = __esm({
16050
+ "src/git/orphan-cleanup.ts"() {
16051
+ "use strict";
16052
+ import_node_fs43 = require("fs");
16053
+ import_node_path43 = require("path");
16054
+ init_worktree();
16055
+ init_thread_store();
16056
+ init_paths();
16057
+ init_app_settings();
16058
+ }
16059
+ });
16060
+
16061
+ // src/store/history-cleanup.ts
16062
+ function stripArchivedTranscript(thread) {
16063
+ writeThread(
16064
+ {
16065
+ ...thread,
16066
+ attachments: [],
16067
+ pendingTurnAttachments: [],
16068
+ queueAttachments: [],
16069
+ compactedMessages: void 0,
16070
+ messages: [historyRetentionStubMessage(thread)]
16071
+ },
16072
+ { touch: false }
16073
+ );
16074
+ }
16075
+ function cleanupArchivedHistory(opts = {}) {
16076
+ const settings = loadAppSettings();
16077
+ if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
16078
+ return { stripped: [], purged: [] };
16079
+ }
16080
+ const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
16081
+ const nowMs = opts.nowMs ?? Date.now();
16082
+ const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
16083
+ maxCount: opts.maxCount ?? historyMaxCount(settings),
16084
+ maxDays: opts.maxDays ?? historyMaxDays(settings),
16085
+ nowMs
16086
+ });
16087
+ const stripped = [];
16088
+ const purged = [];
16089
+ if (opts.dryRun) {
16090
+ return { stripped: plan.stripIds, purged: plan.purgeIds };
16091
+ }
16092
+ for (const id of plan.stripIds) {
16093
+ const thread = readThread(id);
16094
+ if (!thread || thread.status !== "archived") continue;
16095
+ try {
16096
+ stripArchivedTranscript(thread);
16097
+ stripped.push(id);
16098
+ } catch {
16099
+ }
16100
+ }
16101
+ for (const id of plan.purgeIds) {
16102
+ const thread = readThread(id);
16103
+ if (!thread || thread.status !== "archived") continue;
16104
+ try {
16105
+ deleteThreadRecord(id);
16106
+ purged.push(id);
16107
+ } catch {
16108
+ }
16109
+ }
16110
+ if (stripped.length > 0 || purged.length > 0) {
16111
+ updateAdvancedSettings({
16112
+ historyLastCleanupAt: new Date(nowMs).toISOString()
16113
+ });
16114
+ }
16115
+ return { stripped, purged };
16116
+ }
16117
+ function shouldRunHistoryCleanup(settings = loadAppSettings()) {
16118
+ if (!autoCleanupHistoryEnabled(settings)) return false;
16119
+ const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
16120
+ const last = settings.advanced.historyLastCleanupAt;
16121
+ if (!last) return true;
16122
+ const elapsed = Date.now() - Date.parse(last);
16123
+ return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
16124
+ }
16125
+ var init_history_cleanup = __esm({
16126
+ "src/store/history-cleanup.ts"() {
16127
+ "use strict";
16128
+ init_app_settings();
16129
+ init_history_retention();
16130
+ init_thread_store();
16131
+ }
16132
+ });
16133
+
16134
+ // src/git/apply-into-main.ts
16135
+ async function applyThreadIntoMain(thread, opts) {
16136
+ const method = opts?.method ?? "merge";
16137
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
16138
+ const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
16139
+ cwd: thread.repoPath,
16140
+ reject: false
16141
+ });
16142
+ if (status.stdout.trim()) {
16143
+ throw new Error(
16144
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
16145
+ );
16146
+ }
16147
+ const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
16148
+ cwd: thread.repoPath,
16149
+ reject: false
16150
+ });
16151
+ if (checkout.exitCode !== 0) {
16152
+ throw new Error(
16153
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
16154
+ );
16155
+ }
16156
+ if (method === "cherry-pick") {
16157
+ const log = await (0, import_execa5.execa)(
16158
+ "git",
16159
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
16160
+ { cwd: thread.repoPath, reject: false }
16161
+ );
16162
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
16163
+ if (!shas.length) {
16164
+ return {
16165
+ applied: false,
16166
+ method,
16167
+ targetBranch: target,
16168
+ message: `No commits on ${thread.branchName} not already in ${target}`
16169
+ };
16170
+ }
16171
+ for (const sha of shas) {
16172
+ const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
16173
+ cwd: thread.repoPath,
16174
+ reject: false
16175
+ });
16176
+ if (cp.exitCode !== 0) {
16177
+ await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
16178
+ cwd: thread.repoPath,
16179
+ reject: false
16180
+ });
16181
+ throw new Error(
16182
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
16183
+ );
16184
+ }
16185
+ }
16168
16186
  return {
16169
- stillRunning: false,
16170
- ok: false,
16171
- failed: true,
16172
- status: "failed",
16173
- id: jobId,
16174
- delta: "",
16175
- progress: "No detached job. Start one first.",
16176
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
16187
+ applied: true,
16188
+ method,
16189
+ targetBranch: target,
16190
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
16177
16191
  };
16178
16192
  }
16179
- const deadline = Date.now() + timeoutMs;
16180
- let snap = snapshotJob(dir);
16181
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
16182
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
16183
- return toResult(jobId, snap, {
16184
- failed: true,
16185
- progress: "No detached job. Start one first."
16186
- });
16193
+ const merge = await (0, import_execa5.execa)(
16194
+ "git",
16195
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
16196
+ { cwd: thread.repoPath, reject: false }
16197
+ );
16198
+ if (merge.exitCode !== 0) {
16199
+ await (0, import_execa5.execa)("git", ["merge", "--abort"], {
16200
+ cwd: thread.repoPath,
16201
+ reject: false
16202
+ });
16203
+ throw new Error(
16204
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
16205
+ );
16206
+ }
16207
+ return {
16208
+ applied: true,
16209
+ method: "merge",
16210
+ targetBranch: target,
16211
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
16212
+ };
16213
+ }
16214
+ var import_execa5;
16215
+ var init_apply_into_main = __esm({
16216
+ "src/git/apply-into-main.ts"() {
16217
+ "use strict";
16218
+ import_execa5 = require("execa");
16219
+ init_worktree();
16220
+ }
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";
16187
16231
  }
16188
- while (Date.now() < deadline) {
16189
- snap = snapshotJob(dir);
16190
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
16191
- if (!snap.running) return toResult(jobId, snap, { failed: true });
16192
- 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 };
16193
16238
  }
16194
- snap = snapshotJob(dir);
16195
- 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 };
16196
16248
  }
16197
- function killProcessTree(pid, signal) {
16198
- if (!Number.isInteger(pid) || pid <= 0) return;
16199
- if (process.platform !== "win32") {
16200
- try {
16201
- process.kill(-pid, signal);
16202
- return;
16203
- } catch {
16204
- }
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";
16205
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() {
16206
16277
  try {
16207
- 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;
16208
16281
  } catch {
16282
+ return null;
16209
16283
  }
16210
16284
  }
16211
- function appendJobLog(logFile, line) {
16285
+ function pidAlive(pid) {
16212
16286
  try {
16213
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(logFile), { recursive: true });
16214
- const body = line.endsWith("\n") ? line : `${line}
16215
- `;
16216
- (0, import_node_fs45.appendFileSync)(logFile, body);
16287
+ process.kill(pid, 0);
16288
+ return true;
16217
16289
  } catch {
16290
+ return false;
16218
16291
  }
16219
16292
  }
16220
- async function stopDetachedJob(cwd, id, opts) {
16221
- const jobId = sanitizeDetachedJobId(id);
16222
- const root = cwd.trim() || process.cwd();
16223
- const dir = resolveJobDir(root, jobId);
16224
- const sleep = opts?.sleep ?? sleepMs;
16225
- const why = opts?.reason?.trim() ?? "";
16226
- if (!(0, import_node_fs45.existsSync)(dir)) {
16227
- return {
16228
- stillRunning: false,
16229
- ok: false,
16230
- failed: true,
16231
- status: "failed",
16232
- id: jobId,
16233
- delta: "",
16234
- progress: "No detached job. Start one first.",
16235
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
16236
- stopped: false,
16237
- reason: "not-found"
16238
- };
16239
- }
16240
- let snap = snapshotJob(dir);
16241
- if (!snap.running) {
16242
- const result2 = toResult(jobId, snap);
16243
- return {
16244
- ...result2,
16245
- stopped: false,
16246
- reason: "not-running",
16247
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
16248
- };
16249
- }
16250
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
16251
- const targetPid = snap.pid;
16252
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
16253
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
16254
- const deadline = Date.now() + graceMs;
16255
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
16256
- await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
16257
- }
16258
- if (targetPid != null) {
16259
- killProcessTree(targetPid, "SIGKILL");
16260
- await sleep(100);
16261
- }
16262
- const exitFile = (0, import_node_path45.join)(dir, "exit");
16263
- if (!(0, import_node_fs45.existsSync)(exitFile)) {
16264
- try {
16265
- (0, import_node_fs45.writeFileSync)(exitFile, "1\n");
16266
- } catch {
16267
- }
16268
- }
16269
- snap = snapshotJob(dir);
16270
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
16271
- const result = toResult(jobId, snap, { failed: true });
16272
- const stopped = !stillRunning;
16273
- return {
16274
- ...result,
16275
- stillRunning,
16276
- ok: false,
16277
- failed: true,
16278
- status: stillRunning ? "running" : "failed",
16279
- stopped,
16280
- reason: stopped ? "stopped" : "still-running",
16281
- stopReason: why || void 0,
16282
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
16283
- };
16293
+ function isDesktopHostAlive() {
16294
+ const pid = readDesktopHostPid();
16295
+ return pid != null && pidAlive(pid);
16284
16296
  }
16285
- 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;
16286
- var init_wait_for_job = __esm({
16287
- "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"() {
16288
16307
  "use strict";
16289
16308
  import_node_fs45 = require("fs");
16290
16309
  import_node_path45 = require("path");
16291
- init_workspace_scratch();
16292
- init_wait_for_turn();
16293
- MAX_JOB_CONTINUES = 8;
16294
- MAX_JOB_DELTA_LINES = 80;
16295
- MAX_JOB_DELTA_CHARS = 4e3;
16296
- DEFERRED_DONE_MAX_CHARS = 480;
16297
- GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
16298
- 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.";
16299
- 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.";
16300
- 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();
16301
16361
  }
16302
16362
  });
16303
16363
 
@@ -21364,6 +21424,13 @@ function isPresentPlanToolName(name) {
21364
21424
  if (!name) return false;
21365
21425
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
21366
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
+ }
21367
21434
  function extractPresentedPlan(parts) {
21368
21435
  if (!parts?.length) return null;
21369
21436
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -21401,7 +21468,7 @@ function resolvePlanMarkdown(opts) {
21401
21468
  }
21402
21469
  return null;
21403
21470
  }
21404
- 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;
21405
21472
  var init_plan_present = __esm({
21406
21473
  "src/plan/plan-present.ts"() {
21407
21474
  "use strict";
@@ -21409,6 +21476,8 @@ var init_plan_present = __esm({
21409
21476
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
21410
21477
  PLAN_FILE_NAME = "plan.md";
21411
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.";
21412
21481
  }
21413
21482
  });
21414
21483
 
@@ -21418,11 +21487,15 @@ __export(plan_file_exports, {
21418
21487
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
21419
21488
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
21420
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,
21421
21492
  extractPresentedPlan: () => extractPresentedPlan,
21422
21493
  isPresentPlanToolName: () => isPresentPlanToolName,
21423
21494
  planFileAbs: () => planFileAbs,
21424
21495
  readPlanFile: () => readPlanFile,
21425
21496
  resolvePlanMarkdown: () => resolvePlanMarkdown,
21497
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
21498
+ threadMayPresentPlan: () => threadMayPresentPlan,
21426
21499
  writePlanFile: () => writePlanFile
21427
21500
  });
21428
21501
  function ensureAttachmentsGitignore(worktreePath) {
@@ -23090,7 +23163,8 @@ var init_orchestrator = __esm({
23090
23163
  if (compact.didCompact) {
23091
23164
  thread = updateThread(threadId, {
23092
23165
  messages: compact.thread.messages,
23093
- sessionId: compact.thread.sessionId
23166
+ sessionId: compact.thread.sessionId,
23167
+ compactedMessages: compact.thread.compactedMessages
23094
23168
  });
23095
23169
  this.emit({
23096
23170
  type: "context_compacted",
@@ -23104,9 +23178,6 @@ var init_orchestrator = __esm({
23104
23178
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
23105
23179
  thread = this.requireThread(threadId);
23106
23180
  }
23107
- if (this.stoppedTurns.has(threadId)) {
23108
- return;
23109
- }
23110
23181
  const promptText = typeof prompt === "string" ? prompt : "";
23111
23182
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
23112
23183
  const parked = takePendingTurnAttachments(thread);
@@ -23123,6 +23194,14 @@ var init_orchestrator = __esm({
23123
23194
  agent: thread.agent,
23124
23195
  orch: isOrchestratorThread(thread)
23125
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
+ }
23126
23205
  thread = this.requireThread(threadId);
23127
23206
  const gitPrompt = expandCanonicalGitRequest(promptText);
23128
23207
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -23181,7 +23260,7 @@ var init_orchestrator = __esm({
23181
23260
  ].filter(Boolean).join("\n\n") || void 0;
23182
23261
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
23183
23262
  const agentPrompt = [
23184
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
23263
+ planModeTurnInstruction(thread.planMode),
23185
23264
  orchestrationReminder,
23186
23265
  prGateDirective,
23187
23266
  injectedNoticeContext,
@@ -26609,6 +26688,7 @@ function mcpArchiveBlockedReason(thread) {
26609
26688
 
26610
26689
  // src/mcp/server.ts
26611
26690
  init_profile();
26691
+ init_plan_present();
26612
26692
 
26613
26693
  // src/mcp/run-script-ref.ts
26614
26694
  init_chat_tabs();
@@ -29539,31 +29619,40 @@ async function startMcpServer() {
29539
29619
  return mcpJson(payload);
29540
29620
  }
29541
29621
  );
29542
- server.tool(
29543
- "present_plan",
29544
- "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.",
29545
- {
29546
- title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
29547
- content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
29548
- thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
29549
- },
29550
- async ({ title, content, thread_id }) => {
29551
- const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
29552
- let root = process.cwd();
29553
- if (thread_id?.trim()) {
29554
- 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();
29555
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);
29556
29653
  }
29557
- const path = writePlanFile2(root, content);
29558
- const payload = {
29559
- ok: true,
29560
- path,
29561
- title: title?.trim() || "Plan",
29562
- message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
29563
- };
29564
- return mcpJson(payload);
29565
- }
29566
- );
29654
+ );
29655
+ }
29567
29656
  server.tool(
29568
29657
  "present_schema",
29569
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.",