@sideboard-ai/core 0.1.242 → 0.1.244

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-PBRIHUYM.js → agents-BQZ2CHXK.js} +9 -5
  8. package/dist/{agents-CDQCQVWU.js → agents-Q23R4WOR.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-3Q4PVZ3U.js → chunk-2QRU4IFI.js} +32 -5
  12. package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
  13. package/dist/{chunk-LRMJ6V6U.js → chunk-4HMV53AB.js} +1506 -1465
  14. package/dist/{chunk-OKHOYI53.js → chunk-576S3HJ7.js} +5 -5
  15. package/dist/{chunk-TB6UMHIF.js → chunk-5ZFULOTC.js} +10 -2
  16. package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
  17. package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
  18. package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
  19. package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
  20. package/dist/{chunk-QXYWLO6W.js → chunk-CYYQ6577.js} +28 -5
  21. package/dist/{chunk-O3UAQAYA.js → chunk-E6C5NR46.js} +10 -2
  22. package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
  23. package/dist/{chunk-BILEJG3V.js → chunk-GFRC6JQN.js} +5 -5
  24. package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
  25. package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
  26. package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
  27. package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
  28. package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
  29. package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
  30. package/dist/{chunk-R3YKDTVE.js → chunk-YJJ2A5CI.js} +1572 -1529
  31. package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
  32. package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-WQIICPT6.js} +4 -4
  33. package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-ZBYRREHD.js} +4 -4
  34. package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-2N6KISDO.js} +4 -4
  35. package/dist/{global-workspace-TD5SARNW.js → global-workspace-3SQU4GL4.js} +4 -4
  36. package/dist/index.cjs +590 -470
  37. package/dist/index.d.cts +50 -3
  38. package/dist/index.d.ts +50 -3
  39. package/dist/index.js +73 -39
  40. package/dist/mcp/run-stdio.cjs +1649 -1552
  41. package/dist/mcp/run-stdio.js +51 -37
  42. package/dist/{orchestrator-VYNPNHY4.js → orchestrator-ABF6QFE6.js} +7 -7
  43. package/dist/{orchestrator-66ZFQHW5.js → orchestrator-ZJ6SYGW7.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-JX5R4V24.js → workspaces-CFI7XVAE.js} +4 -4
  49. package/dist/{workspaces-4SFSCYKT.js → workspaces-QLKWW3O2.js} +4 -4
  50. package/dist/{worktree-AGVPKDBH.js → worktree-4UAOZUJ4.js} +5 -3
  51. package/dist/{worktree-Y5FES5D6.js → worktree-PK3QETXO.js} +5 -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;
@@ -6159,6 +6161,7 @@ __export(worktree_exports, {
6159
6161
  getPrForHeadBranch: () => getPrForHeadBranch,
6160
6162
  getPrMeta: () => getPrMeta,
6161
6163
  ghHeadRef: () => ghHeadRef,
6164
+ ghPrCreateAssigneeArgs: () => ghPrCreateAssigneeArgs,
6162
6165
  ghRepoSelectArgs: () => ghRepoSelectArgs,
6163
6166
  githubAgentGitEnv: () => githubAgentGitEnv,
6164
6167
  isDefaultishSourceRef: () => isDefaultishSourceRef,
@@ -7408,6 +7411,11 @@ async function mergePr(cwd, selector, opts) {
7408
7411
  }
7409
7412
  return { url, state: "MERGED" };
7410
7413
  }
7414
+ function ghPrCreateAssigneeArgs(assignee) {
7415
+ const raw = assignee?.trim();
7416
+ if (raw && /^(none|unassigned)$/i.test(raw)) return [];
7417
+ return ["--assignee", raw || "@me"];
7418
+ }
7411
7419
  async function createOrUpdatePr(worktreePath, opts) {
7412
7420
  await ensureGhPreferOrigin(worktreePath);
7413
7421
  const slug = await resolveGithubRepoSlug(worktreePath);
@@ -7482,6 +7490,7 @@ async function createOrUpdatePrWithBodyFile(worktreePath, opts) {
7482
7490
  headRef
7483
7491
  ];
7484
7492
  if (opts.draft) args2.push("--draft");
7493
+ args2.push(...ghPrCreateAssigneeArgs(opts.assignee));
7485
7494
  await gh(args2, worktreePath, { reject: false });
7486
7495
  const again = await gh(
7487
7496
  [...repoArgs, "pr", "view", branchOnly, "--json", "url", "--jq", ".url"],
@@ -7503,6 +7512,7 @@ async function createOrUpdatePrWithBodyFile(worktreePath, opts) {
7503
7512
  headRef
7504
7513
  ];
7505
7514
  if (opts.draft) args.push("--draft");
7515
+ args.push(...ghPrCreateAssigneeArgs(opts.assignee));
7506
7516
  const created = await gh(args, worktreePath, { reject: false });
7507
7517
  if (created.exitCode !== 0) {
7508
7518
  const raw = created.stderr.trim() || created.stdout.trim() || "gh pr create failed";
@@ -8097,7 +8107,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
8097
8107
  "- Examples:",
8098
8108
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
8099
8109
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
8100
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
8110
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft --assignee @me -R <origin-owner/name>`).",
8101
8111
  "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
8102
8112
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
8103
8113
  ].join("\n");
@@ -8306,7 +8316,7 @@ var init_coordinator_prompt = __esm({
8306
8316
  '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .claude/skills/review/SKILL.md when present, else .context/review.md copied from .sideboard/review.md / stock; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id. The review stays in that chat so they can read it and type next steps \u2014 do not tell the child to comment or update the PR or ticket.',
8307
8317
  "- ask_git \u2014 commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same prompts as the desktop git buttons (Resolve, Create PR, \u2026). Always steers the worktree agent; then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing. If the user gave a goal (Greptile 5/5, CI green), send_to_thread that goal so the worktree agent enters the watch-fix-push loop \u2014 do not tell the human to poll.",
8308
8318
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
8309
- '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Ready for review.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
8319
+ '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Ready for review.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft --assignee @me -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
8310
8320
  "Process guides:",
8311
8321
  "- Long child jobs (pack, test, deploy, anything that may run more than ~30s): workers always have `/long-running` (Sideboard product skill). Tell them to detach and wait; they may `stop_job` if it hangs or is doing the wrong thing; do not ask the human to poll.",
8312
8322
  "- Recurring multi-item / fan-out: if the child worktree has `.claude/skills/graph-engineering/SKILL.md`, tell the worker to follow it (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch three threads.",
@@ -9704,6 +9714,10 @@ var init_node_launch = __esm({
9704
9714
  // src/agents/injected-mcp.ts
9705
9715
  function sideboardWorktreeAllowedTools(opts) {
9706
9716
  const out = [...SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS];
9717
+ if (!opts?.planMode) {
9718
+ const idx = out.indexOf("mcp__sideboard__present_plan");
9719
+ if (idx >= 0) out.splice(idx, 1);
9720
+ }
9707
9721
  if (opts?.github !== false) out.push(...SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS);
9708
9722
  if (opts?.linear) out.push(...SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS);
9709
9723
  if (opts?.abletime) out.push(...SIDEBOARD_ABLETIME_MCP_ALLOWED_TOOLS);
@@ -10564,6 +10578,9 @@ function skillToolFromTextPart(part, id) {
10564
10578
  updatedAt: Date.now()
10565
10579
  };
10566
10580
  }
10581
+ function compactSummaryPart(text7, id = "compact-summary") {
10582
+ return compactToolPart(text7, id, { status: "done" });
10583
+ }
10567
10584
  function compactToolPart(text7, id, opts) {
10568
10585
  const input = { trigger: opts?.trigger ?? "auto" };
10569
10586
  return withPartTimes(
@@ -10841,6 +10858,17 @@ function partsToAssistantText(parts) {
10841
10858
  (p) => p.type === "text" && !messagePartParentId(p)
10842
10859
  ).map((p) => p.text).join("").trim();
10843
10860
  }
10861
+ function lastAssistantMessageText(parts) {
10862
+ if (!parts?.length) return "";
10863
+ for (let i = parts.length - 1; i >= 0; i--) {
10864
+ const p = parts[i];
10865
+ if (!p || p.type !== "text" || messagePartParentId(p)) continue;
10866
+ const text7 = p.text.trim();
10867
+ if (!text7 || isInternalAgentStatusText(text7)) continue;
10868
+ return text7;
10869
+ }
10870
+ return "";
10871
+ }
10844
10872
  function stripBrightsyNdjsonNoise(text7) {
10845
10873
  if (!text7 || !text7.includes('"type"')) return text7;
10846
10874
  let out = text7;
@@ -10878,6 +10906,9 @@ var init_message_parts = __esm({
10878
10906
  });
10879
10907
 
10880
10908
  // src/agents/types.ts
10909
+ function planModeTurnInstruction(planMode) {
10910
+ return planMode ? PLAN_MODE_INSTRUCTION : PLAN_MODE_OFF_INSTRUCTION;
10911
+ }
10881
10912
  function permissionMode(thread) {
10882
10913
  if (thread.sourceType === "orchestration") {
10883
10914
  return {
@@ -10916,11 +10947,12 @@ function permissionMode(thread) {
10916
10947
  codexSandbox: "workspace-write"
10917
10948
  };
10918
10949
  }
10919
- var PLAN_MODE_INSTRUCTION;
10950
+ var PLAN_MODE_INSTRUCTION, PLAN_MODE_OFF_INSTRUCTION;
10920
10951
  var init_types = __esm({
10921
10952
  "src/agents/types.ts"() {
10922
10953
  "use strict";
10923
10954
  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.";
10955
+ 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
10956
  }
10925
10957
  });
10926
10958
 
@@ -11433,7 +11465,8 @@ var init_claude = __esm({
11433
11465
  ...sideboardWorktreeAllowedTools({
11434
11466
  github: true,
11435
11467
  linear: isLinearConnected(),
11436
- abletime: isAbleTimeConnected()
11468
+ abletime: isAbleTimeConnected(),
11469
+ planMode: thread.planMode
11437
11470
  }),
11438
11471
  ...brightsyMcpAllowedTools(injectedBrightsyNames)
11439
11472
  ];
@@ -13488,9 +13521,9 @@ function gitActionExpansion(action, base) {
13488
13521
  case "commit-push":
13489
13522
  return "Commit any uncommitted work with a purpose-stating message, then push to origin (`git push -u origin HEAD`; this updates an existing PR when one is linked). Do not start a checks loop unless a goal was given.";
13490
13523
  case "create-draft":
13491
- return "Commit, push, then create a draft PR with `gh pr create --draft -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked.";
13524
+ return "Commit, push, then create a draft PR with `gh pr create --draft --assignee @me -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked. Use a different `--assignee` only if the user named someone else.";
13492
13525
  case "create-web":
13493
- return "Commit, push, then `gh pr create --web -R <origin-owner/name>`.";
13526
+ return "Commit, push, then `gh pr create --web --assignee @me -R <origin-owner/name>`. Use a different `--assignee` only if the user named someone else.";
13494
13527
  case "resolve-conflicts":
13495
13528
  return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
13496
13529
  case "ready-for-review":
@@ -13983,12 +14016,38 @@ ${match.text}`, "");
13983
14016
  blocks.push("Continue from this context. Do not repeat the summary unless asked.");
13984
14017
  return blocks.join("\n");
13985
14018
  }
14019
+ function compactArchiveProse(message) {
14020
+ if (message.origin === "continue") return null;
14021
+ if (message.role === "summary") return null;
14022
+ let text7 = (message.text ?? "").trim();
14023
+ if (!text7 && message.role === "agent") {
14024
+ text7 = lastAssistantMessageText(message.parts).trim();
14025
+ }
14026
+ if (!text7 || isInternalAgentStatusText(text7)) return null;
14027
+ return { role: message.role, text: text7, ts: message.ts };
14028
+ }
14029
+ function messagesForCompactArchive(older) {
14030
+ const out = [];
14031
+ for (const message of older) {
14032
+ const prose = compactArchiveProse(message);
14033
+ if (prose) out.push(prose);
14034
+ }
14035
+ return out;
14036
+ }
14037
+ function appendCompactedMessages(existing, older) {
14038
+ const next = messagesForCompactArchive(older);
14039
+ if (!existing?.length) return next;
14040
+ if (!next.length) return existing;
14041
+ return [...existing, ...next];
14042
+ }
13986
14043
  function applyCompaction(messages, summaryText, thresholds = {}) {
13987
14044
  const { older, recent } = splitForCompaction(messages, thresholds);
13988
14045
  if (older.length === 0) return messages;
14046
+ const text7 = summaryText.trim();
13989
14047
  const summary = {
13990
14048
  role: "summary",
13991
- text: summaryText.trim(),
14049
+ text: text7,
14050
+ parts: [compactSummaryPart(text7)],
13992
14051
  ts: (/* @__PURE__ */ new Date()).toISOString()
13993
14052
  };
13994
14053
  return [summary, ...recent];
@@ -14016,6 +14075,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
14016
14075
  cwd: thread.worktreePath
14017
14076
  });
14018
14077
  let messages = applyCompaction(thread.messages, summary, thresholds);
14078
+ const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
14019
14079
  const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
14020
14080
  if (resetSession) {
14021
14081
  messages = applyForwardOccupancy(messages);
@@ -14023,6 +14083,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
14023
14083
  const next = {
14024
14084
  ...thread,
14025
14085
  messages,
14086
+ ...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
14026
14087
  sessionId: resetSession ? null : thread.sessionId,
14027
14088
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14028
14089
  };
@@ -14038,6 +14099,7 @@ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAG
14038
14099
  var init_context_compact = __esm({
14039
14100
  "src/composer/context-compact.ts"() {
14040
14101
  "use strict";
14102
+ init_message_parts();
14041
14103
  init_error_detail();
14042
14104
  init_usage();
14043
14105
  init_context_estimate();
@@ -14596,1708 +14658,1714 @@ var init_worktree_run_ports = __esm({
14596
14658
  }
14597
14659
  });
14598
14660
 
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("#"));
14608
- }
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)];
14629
- }
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 {
14638
- }
14639
- if (defaults.length) return defaults;
14640
- return [".env.local", ".env"];
14641
- }
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);
14652
- }
14653
- return copied;
14661
+ // src/mcp/wait-for-turn.ts
14662
+ function mcpWaitForTurnTimeoutMs(requested) {
14663
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
14664
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
14665
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
14654
14666
  }
14655
- async function captureLoginEnv() {
14656
- if (cachedLoginEnv) return { ...cachedLoginEnv };
14657
- const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
14658
- 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
- }
14676
- } catch {
14667
+ function mcpWaitTaskHint(taskState, status) {
14668
+ switch (taskState) {
14669
+ case "submitted":
14670
+ return MCP_WAIT_QUEUED_HINT;
14671
+ case "working":
14672
+ return MCP_WAIT_STILL_RUNNING_HINT;
14673
+ case "input-required":
14674
+ return MCP_WAIT_INPUT_REQUIRED_HINT;
14675
+ case "canceled":
14676
+ return MCP_WAIT_STOPPED_HINT;
14677
+ case "failed":
14678
+ return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
14679
+ default:
14680
+ return void 0;
14677
14681
  }
14678
- cachedLoginEnv = { ...process.env };
14679
- return { ...cachedLoginEnv };
14680
14682
  }
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
- }
14683
+ 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;
14684
+ var init_wait_for_turn = __esm({
14685
+ "src/mcp/wait-for-turn.ts"() {
14686
+ "use strict";
14687
+ MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
14688
+ 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.";
14689
+ 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.";
14690
+ 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.";
14691
+ MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
14692
+ 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.";
14693
+ 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.";
14707
14694
  }
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);
14695
+ });
14696
+
14697
+ // src/mcp/wait-for-job.ts
14698
+ function mcpWaitForJobTimeoutMs(requested) {
14699
+ return mcpWaitForTurnTimeoutMs(requested);
14715
14700
  }
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);
14701
+ function sanitizeDetachedJobId(id) {
14702
+ const s = id.trim();
14703
+ if (!JOB_ID_RE.test(s)) {
14704
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
14722
14705
  }
14723
- return pids;
14706
+ return s;
14724
14707
  }
14725
- function listUnixListenerPids(port, selfPid) {
14708
+ function jobAlive(pid) {
14709
+ if (!Number.isInteger(pid) || pid <= 0) return false;
14726
14710
  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);
14711
+ process.kill(pid, 0);
14712
+ return true;
14733
14713
  } catch {
14734
- return [];
14714
+ return false;
14735
14715
  }
14736
14716
  }
14737
- function listenerCwd(pid) {
14738
- if (!Number.isFinite(pid) || pid <= 0) return null;
14717
+ function processGroupAlive(pgid) {
14718
+ if (process.platform === "win32") return false;
14719
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
14739
14720
  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 {
14721
+ process.kill(-pgid, 0);
14722
+ return true;
14723
+ } catch (err) {
14724
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
14749
14725
  }
14750
- return null;
14751
14726
  }
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
- }
14727
+ function jobTreeAlive(pid) {
14728
+ if (pid == null) return false;
14729
+ return jobAlive(pid) || processGroupAlive(pid);
14758
14730
  }
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;
14731
+ function readIntFile(file) {
14732
+ if (!(0, import_node_fs39.existsSync)(file)) return null;
14733
+ const n = Number.parseInt((0, import_node_fs39.readFileSync)(file, "utf8").trim(), 10);
14734
+ return Number.isInteger(n) ? n : null;
14769
14735
  }
14770
- function signalPid(pid, signal) {
14771
- try {
14772
- process.kill(pid, signal);
14773
- } catch {
14774
- }
14736
+ function jobDir(root, id, legacy = false) {
14737
+ return (0, import_node_path39.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
14775
14738
  }
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);
14739
+ function resolveJobDir(root, id) {
14740
+ const modern = jobDir(root, id, false);
14741
+ if ((0, import_node_fs39.existsSync)(modern)) return modern;
14742
+ const legacy = jobDir(root, id, true);
14743
+ if ((0, import_node_fs39.existsSync)(legacy)) return legacy;
14744
+ return modern;
14780
14745
  }
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");
14746
+ function listJobIdsIn(root, rel) {
14747
+ const dir = (0, import_node_path39.join)(root, rel);
14748
+ if (!(0, import_node_fs39.existsSync)(dir)) return [];
14749
+ return (0, import_node_fs39.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
14750
+ }
14751
+ function listRunningDetachedJobs(worktreePath) {
14752
+ const root = worktreePath.trim();
14753
+ if (!root) return [];
14754
+ const ids = /* @__PURE__ */ new Set([
14755
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
14756
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
14757
+ ]);
14758
+ const running = [];
14759
+ for (const id of ids) {
14760
+ const dir = resolveJobDir(root, id);
14761
+ const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
14762
+ if (jobTreeAlive(pid)) running.push(id);
14789
14763
  }
14764
+ return running.sort();
14790
14765
  }
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
- }
14766
+ function looksLikeDeferredDonePromise(text7) {
14767
+ const t = (text7 ?? "").trim();
14768
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
14769
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
14770
+ if (!unquoted) return false;
14771
+ 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);
14814
14772
  }
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
- }
14832
- }
14773
+ function isJobContinuePrompt(text7) {
14774
+ if (typeof text7 !== "string") return false;
14775
+ const t = text7.trim();
14776
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
14833
14777
  }
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);
14778
+ function collapseGhRunWatchLog(text7) {
14779
+ if (!text7.includes(GH_WATCH_REFRESH)) return text7;
14780
+ const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
14781
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
14782
+ if (watchFrames.length <= 1) return text7;
14783
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
14784
+ const last = watchFrames[watchFrames.length - 1] ?? "";
14785
+ const omitted = watchFrames.length - 1;
14786
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
14787
+ `;
14788
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
14857
14789
  }
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()));
14790
+ function capJobLogDelta(text7) {
14791
+ const collapsed = collapseGhRunWatchLog(text7);
14792
+ const lines = collapsed.split("\n");
14793
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
14794
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
14795
+ if (out.length > MAX_JOB_DELTA_CHARS) {
14796
+ out = `\u2026(truncated ${out.length} chars)
14797
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
14879
14798
  }
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 }
14888
- });
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
- };
14799
+ return out;
14898
14800
  }
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 });
14904
- }
14905
- return handle.done;
14801
+ function formatJobStillRunningContinuePrompt(jobIds) {
14802
+ const ids = jobIds.join(", ");
14803
+ return [
14804
+ `Detached job still running: ${ids}.`,
14805
+ "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
14806
+ "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
14807
+ "Then report the result. Do not tell the user you will let them know later."
14808
+ ].join(" ");
14906
14809
  }
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);
14919
- return {
14920
- ran: true,
14921
- exitCode,
14922
- source,
14923
- kill: handle.kill
14924
- };
14810
+ function formatDeferredDoneContinuePrompt() {
14811
+ return [
14812
+ "You ended the turn after promising to report later, but no detached job is running.",
14813
+ "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).",
14814
+ "Do not say you will let the user know later."
14815
+ ].join(" ");
14925
14816
  }
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
14817
+ function turnWatchedDetachedJob(parts) {
14818
+ return parts.some((p) => {
14819
+ if (p.type !== "tool") return false;
14820
+ if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
14821
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
14822
+ return /detached-job\.(?:js|cjs)\b/i.test(blob);
14935
14823
  });
14936
- const exitCode = await attachAbort(handle, opts?.signal);
14937
- return {
14938
- ran: true,
14939
- exitCode,
14940
- source: found.source,
14941
- kill: handle.kill
14942
- };
14943
14824
  }
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);
14825
+ function planJobContinue(opts) {
14826
+ if (opts.isOrchestrator) return { action: "none" };
14827
+ if (opts.agent === "brightsy") return { action: "none" };
14828
+ if (opts.queueLength > 0) return { action: "none" };
14829
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
14830
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
14831
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
14832
+ return {
14833
+ action: "wait",
14834
+ jobIds: opts.runningJobIds,
14835
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
14836
+ };
14953
14837
  }
14954
- if (!setup.ran) {
14955
- setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
14838
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
14839
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
14956
14840
  }
14957
- return setup;
14958
- }
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 };
14841
+ return { action: "none" };
14969
14842
  }
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
- });
14843
+ function tailProgress(logFile, maxLines = 12) {
14844
+ if (!(0, import_node_fs39.existsSync)(logFile)) return "(no log yet)";
14845
+ const lines = (0, import_node_fs39.readFileSync)(logFile, "utf8").split("\n");
14846
+ return lines.slice(-maxLines).join("\n");
14977
14847
  }
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;
14848
+ function lastProgressLine(progress) {
14849
+ const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
14850
+ return last ? last.slice(0, 72) : void 0;
14982
14851
  }
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;
14988
- }
14989
- return getDefaultRunScript(worktreePath, repoPath);
14852
+ function readLogLines(file) {
14853
+ if (!(0, import_node_fs39.existsSync)(file)) return [];
14854
+ const lines = (0, import_node_fs39.readFileSync)(file, "utf8").split("\n");
14855
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
14856
+ return lines;
14990
14857
  }
14991
- function getRunMode(worktreePath, repoPath) {
14992
- return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
14858
+ function takeDelta(logFile, cursorFile) {
14859
+ const lines = readLogLines(logFile);
14860
+ const cursor = readIntFile(cursorFile) ?? 0;
14861
+ const start = Math.min(Math.max(0, cursor), lines.length);
14862
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
14993
14863
  }
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 });
15039
- });
15040
- });
14864
+ function snapshotJob(dir) {
14865
+ const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
14866
+ const running = jobTreeAlive(pid);
14867
+ return {
14868
+ pid,
14869
+ running,
14870
+ exitCode: readIntFile((0, import_node_path39.join)(dir, "exit")),
14871
+ log: (0, import_node_path39.join)(dir, "log"),
14872
+ cursor: (0, import_node_path39.join)(dir, "present.cursor"),
14873
+ progress: tailProgress((0, import_node_path39.join)(dir, "log"))
14874
+ };
15041
14875
  }
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;
14876
+ function toResult(id, snap, extra) {
14877
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
14878
+ const ok = !snap.running && snap.exitCode === 0;
14879
+ const stillRunning = snap.running && !ok;
14880
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
14881
+ try {
14882
+ (0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(snap.cursor), { recursive: true });
14883
+ (0, import_node_fs39.writeFileSync)(snap.cursor, `${nextCursor}
14884
+ `);
14885
+ } catch {
15051
14886
  }
15052
- return out;
14887
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
14888
+ const progress = extra?.progress ?? snap.progress;
14889
+ return {
14890
+ stillRunning,
14891
+ ok,
14892
+ failed: Boolean(failed && !stillRunning && !ok),
14893
+ status,
14894
+ id,
14895
+ pid: snap.pid,
14896
+ exitCode: snap.exitCode ?? void 0,
14897
+ phase: lastProgressLine(progress),
14898
+ delta,
14899
+ progress,
14900
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
14901
+ };
15053
14902
  }
15054
- function delay(ms) {
15055
- return new Promise((resolve2) => setTimeout(resolve2, ms));
14903
+ async function sleepMs(ms) {
14904
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
15056
14905
  }
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
- }
15076
- }
15077
- return held;
15078
- } catch (err) {
15079
- await Promise.all(held.map((h) => h.release()));
15080
- throw err;
14906
+ async function waitForDetachedJob(cwd, id, opts) {
14907
+ const jobId = sanitizeDetachedJobId(id);
14908
+ const root = cwd.trim() || process.cwd();
14909
+ const dir = resolveJobDir(root, jobId);
14910
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
14911
+ const sleep = opts?.sleep ?? sleepMs;
14912
+ if (!(0, import_node_fs39.existsSync)(dir)) {
14913
+ return {
14914
+ stillRunning: false,
14915
+ ok: false,
14916
+ failed: true,
14917
+ status: "failed",
14918
+ id: jobId,
14919
+ delta: "",
14920
+ progress: "No detached job. Start one first.",
14921
+ hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
14922
+ };
15081
14923
  }
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);
14924
+ const deadline = Date.now() + timeoutMs;
14925
+ let snap = snapshotJob(dir);
14926
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
14927
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
14928
+ return toResult(jobId, snap, {
14929
+ failed: true,
14930
+ progress: "No detached job. Start one first."
14931
+ });
15088
14932
  }
15089
- return held;
15090
- }
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);
14933
+ while (Date.now() < deadline) {
14934
+ snap = snapshotJob(dir);
14935
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
14936
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
14937
+ await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
15106
14938
  }
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");
14939
+ snap = snapshotJob(dir);
14940
+ return toResult(jobId, snap);
14941
+ }
14942
+ function killProcessTree(pid, signal) {
14943
+ if (!Number.isInteger(pid) || pid <= 0) return;
14944
+ if (process.platform !== "win32") {
14945
+ try {
14946
+ process.kill(-pid, signal);
14947
+ return;
14948
+ } catch {
15113
14949
  }
15114
14950
  }
15115
- return null;
15116
- }
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);
14951
+ try {
14952
+ process.kill(pid, signal);
14953
+ } catch {
15122
14954
  }
15123
- const held = [await reservePort()];
15124
- return fillPortRange(held, size);
15125
14955
  }
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);
14956
+ function appendJobLog(logFile, line) {
15136
14957
  try {
15137
- const handle = await spawnWorkspaceScript(script.command, {
15138
- worktreePath,
15139
- repoPath,
15140
- ports,
15141
- heldPorts: held,
15142
- defaultBranch: opts?.defaultBranch,
15143
- onLine
15144
- });
14958
+ (0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(logFile), { recursive: true });
14959
+ const body = line.endsWith("\n") ? line : `${line}
14960
+ `;
14961
+ (0, import_node_fs39.appendFileSync)(logFile, body);
14962
+ } catch {
14963
+ }
14964
+ }
14965
+ async function stopDetachedJob(cwd, id, opts) {
14966
+ const jobId = sanitizeDetachedJobId(id);
14967
+ const root = cwd.trim() || process.cwd();
14968
+ const dir = resolveJobDir(root, jobId);
14969
+ const sleep = opts?.sleep ?? sleepMs;
14970
+ const why = opts?.reason?.trim() ?? "";
14971
+ if (!(0, import_node_fs39.existsSync)(dir)) {
15145
14972
  return {
15146
- pid: handle.pid,
15147
- port: ports[0],
15148
- ports,
15149
- scriptName: script.name,
15150
- kill: handle.kill,
15151
- done: handle.done
14973
+ stillRunning: false,
14974
+ ok: false,
14975
+ failed: true,
14976
+ status: "failed",
14977
+ id: jobId,
14978
+ delta: "",
14979
+ progress: "No detached job. Start one first.",
14980
+ hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
14981
+ stopped: false,
14982
+ reason: "not-found"
15152
14983
  };
15153
- } catch (err) {
15154
- await Promise.all(held.map((h) => h.release()));
15155
- throw err;
15156
14984
  }
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"() {
15161
- "use strict";
15162
- import_node_fs39 = require("fs");
15163
- import_node_child_process4 = require("child_process");
15164
- import_node_net = require("net");
15165
- 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;
14985
+ let snap = snapshotJob(dir);
14986
+ if (!snap.running) {
14987
+ const result2 = toResult(jobId, snap);
14988
+ return {
14989
+ ...result2,
14990
+ stopped: false,
14991
+ reason: "not-running",
14992
+ hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
14993
+ };
15183
14994
  }
15184
- });
15185
-
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;
15194
- }
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
- };
15203
- }
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;
14995
+ appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
14996
+ const targetPid = snap.pid;
14997
+ if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
14998
+ const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
14999
+ const deadline = Date.now() + graceMs;
15000
+ while (Date.now() < deadline && jobTreeAlive(targetPid)) {
15001
+ await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
15002
+ }
15003
+ if (targetPid != null) {
15004
+ killProcessTree(targetPid, "SIGKILL");
15005
+ await sleep(100);
15006
+ }
15007
+ const exitFile = (0, import_node_path39.join)(dir, "exit");
15008
+ if (!(0, import_node_fs39.existsSync)(exitFile)) {
15009
+ try {
15010
+ (0, import_node_fs39.writeFileSync)(exitFile, "1\n");
15011
+ } catch {
15012
+ }
15210
15013
  }
15014
+ snap = snapshotJob(dir);
15015
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
15016
+ const result = toResult(jobId, snap, { failed: true });
15017
+ const stopped = !stillRunning;
15211
15018
  return {
15212
- output,
15213
- running: mem.running,
15214
- exitCode: mem.exitCode,
15215
- source: mem.source
15019
+ ...result,
15020
+ stillRunning,
15021
+ ok: false,
15022
+ failed: true,
15023
+ status: stillRunning ? "running" : "failed",
15024
+ stopped,
15025
+ reason: stopped ? "stopped" : "still-running",
15026
+ stopReason: why || void 0,
15027
+ hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
15216
15028
  };
15217
15029
  }
15218
- function setupLogPath(threadId) {
15219
- return (0, import_node_path40.join)(threadsDir(), `${threadId}.setup.log.json`);
15220
- }
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)}`;
15030
+ 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;
15031
+ var init_wait_for_job = __esm({
15032
+ "src/mcp/wait-for-job.ts"() {
15033
+ "use strict";
15034
+ import_node_fs39 = require("fs");
15035
+ import_node_path39 = require("path");
15036
+ init_workspace_scratch();
15037
+ init_wait_for_turn();
15038
+ MAX_JOB_CONTINUES = 8;
15039
+ MAX_JOB_DELTA_LINES = 80;
15040
+ MAX_JOB_DELTA_CHARS = 4e3;
15041
+ DEFERRED_DONE_MAX_CHARS = 480;
15042
+ GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
15043
+ 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.";
15044
+ 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.";
15045
+ JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
15046
+ }
15047
+ });
15048
+
15049
+ // src/hook/conductor.ts
15050
+ function matchSimpleGlob(pattern, name) {
15051
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
15052
+ return new RegExp(`^${escaped}$`).test(name);
15224
15053
  }
15225
- function emptySetupLog() {
15226
- return { ...EMPTY };
15054
+ function readWorktreeInclude(repoPath) {
15055
+ const path = (0, import_node_path40.join)(repoPath, ".worktreeinclude");
15056
+ if (!(0, import_node_fs40.existsSync)(path)) return [];
15057
+ return (0, import_node_fs40.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15227
15058
  }
15228
- function persistNow(threadId) {
15229
- const timer = persistTimers.get(threadId);
15230
- if (timer) {
15231
- clearTimeout(timer);
15232
- persistTimers.delete(threadId);
15059
+ function resolveFilesToCopy(repoPath) {
15060
+ const fromInclude = readWorktreeInclude(repoPath);
15061
+ if (fromInclude.length) return fromInclude;
15062
+ const settings = loadRepoSettings(repoPath);
15063
+ if (settings?.filesToCopy?.length) return settings.filesToCopy;
15064
+ if (settings?.fileIncludeGlobs?.length) {
15065
+ const matched = [];
15066
+ try {
15067
+ for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
15068
+ if (!entry.isFile()) continue;
15069
+ for (const glob of settings.fileIncludeGlobs) {
15070
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path40.basename)(glob), entry.name)) {
15071
+ matched.push(entry.name);
15072
+ break;
15073
+ }
15074
+ }
15075
+ }
15076
+ } catch {
15077
+ }
15078
+ if (matched.length) return [...new Set(matched)];
15233
15079
  }
15234
- const mem = memory.get(threadId);
15235
- if (!mem) return;
15080
+ const defaults = [];
15236
15081
  try {
15237
- (0, import_node_fs40.writeFileSync)(
15238
- setupLogPath(threadId),
15239
- `${JSON.stringify(materialize(mem))}
15240
- `,
15241
- "utf8"
15242
- );
15082
+ for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
15083
+ if (entry.isFile() && entry.name.startsWith(".env")) {
15084
+ defaults.push(entry.name);
15085
+ }
15086
+ }
15243
15087
  } catch {
15244
15088
  }
15089
+ if (defaults.length) return defaults;
15090
+ return [".env.local", ".env"];
15245
15091
  }
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
- );
15092
+ function copyConfiguredFiles(repoPath, worktreePath) {
15093
+ const patterns = resolveFilesToCopy(repoPath);
15094
+ const copied = [];
15095
+ for (const rel of patterns) {
15096
+ const src = (0, import_node_path40.join)(repoPath, rel);
15097
+ if (!(0, import_node_fs40.existsSync)(src)) continue;
15098
+ const dest = (0, import_node_path40.join)(worktreePath, rel);
15099
+ (0, import_node_fs40.mkdirSync)((0, import_node_path40.dirname)(dest), { recursive: true });
15100
+ (0, import_node_fs40.copyFileSync)(src, dest);
15101
+ copied.push(rel);
15102
+ }
15103
+ return copied;
15255
15104
  }
15256
- function loadFromDisk(threadId) {
15257
- const path = setupLogPath(threadId);
15258
- if (!(0, import_node_fs40.existsSync)(path)) return null;
15105
+ async function captureLoginEnv() {
15106
+ if (cachedLoginEnv) return { ...cachedLoginEnv };
15107
+ const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
15259
15108
  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
- };
15109
+ const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
15110
+ reject: false,
15111
+ timeout: 5e3
15112
+ });
15113
+ if (result.exitCode === 0 && result.stdout) {
15114
+ const env = { ...process.env };
15115
+ for (const entry of result.stdout.split("\0")) {
15116
+ if (!entry) continue;
15117
+ const eq = entry.indexOf("=");
15118
+ if (eq <= 0) continue;
15119
+ const key = entry.slice(0, eq);
15120
+ const value = entry.slice(eq + 1);
15121
+ if (key) env[key] = value;
15122
+ }
15123
+ cachedLoginEnv = env;
15124
+ return { ...env };
15125
+ }
15267
15126
  } catch {
15268
- return null;
15269
15127
  }
15128
+ cachedLoginEnv = { ...process.env };
15129
+ return { ...cachedLoginEnv };
15270
15130
  }
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;
15131
+ function buildWorkspaceScriptEnv(opts, baseEnv) {
15132
+ const env = stripNestedElectronEnv({
15133
+ ...baseEnv ?? process.env
15134
+ });
15135
+ const name = opts.workspaceName ?? (0, import_node_path40.basename)(opts.worktreePath);
15136
+ const ports = opts.ports ?? [];
15137
+ const primary = ports[0];
15138
+ env.SIDEBOARD_WORKSPACE_NAME = name;
15139
+ env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
15140
+ env.SIDEBOARD_ROOT_PATH = opts.repoPath;
15141
+ env.SIDEBOARD_IS_LOCAL = "1";
15142
+ if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
15143
+ env.CONDUCTOR_WORKSPACE_NAME = name;
15144
+ env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
15145
+ env.CONDUCTOR_ROOT_PATH = opts.repoPath;
15146
+ env.CONDUCTOR_IS_LOCAL = "1";
15147
+ if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
15148
+ if (primary != null) {
15149
+ env.SIDEBOARD_PORT = String(primary);
15150
+ env.CONDUCTOR_PORT = String(primary);
15151
+ env.PORT = String(primary);
15152
+ for (let i = 1; i < ports.length; i++) {
15153
+ const p = ports[i];
15154
+ env[`SIDEBOARD_PORT_${i}`] = String(p);
15155
+ env[`CONDUCTOR_PORT_${i}`] = String(p);
15156
+ }
15285
15157
  }
15286
- return emptySetupLog();
15158
+ applyWorktreePkgCacheEnv(env, opts.worktreePath);
15159
+ return env;
15287
15160
  }
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);
15161
+ function pipeLines(stream, onLine) {
15162
+ if (!stream || !onLine) return;
15163
+ const rl = (0, import_node_readline3.createInterface)({ input: stream });
15164
+ rl.on("line", onLine);
15299
15165
  }
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;
15166
+ function killableListenerPids(raw, selfPid = process.pid) {
15167
+ const pids = [];
15168
+ for (const token of raw.trim().split(/\s+/)) {
15169
+ const pid = Number(token);
15170
+ if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
15171
+ pids.push(pid);
15335
15172
  }
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
- };
15173
+ return pids;
15347
15174
  }
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;
15175
+ function listUnixListenerPids(port, selfPid) {
15176
+ try {
15177
+ const raw = (0, import_node_child_process4.execFileSync)("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
15178
+ encoding: "utf8",
15179
+ stdio: ["ignore", "pipe", "ignore"],
15180
+ timeout: 2e3
15181
+ });
15182
+ return killableListenerPids(raw, selfPid);
15183
+ } catch {
15184
+ return [];
15354
15185
  }
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
15186
  }
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;
15187
+ function listenerCwd(pid) {
15188
+ if (!Number.isFinite(pid) || pid <= 0) return null;
15394
15189
  try {
15395
- (0, import_node_fs41.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15396
- `, "utf8");
15190
+ const raw = (0, import_node_child_process4.execFileSync)("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
15191
+ encoding: "utf8",
15192
+ stdio: ["ignore", "pipe", "ignore"],
15193
+ timeout: 2e3
15194
+ });
15195
+ for (const line of raw.split("\n")) {
15196
+ if (line.startsWith("n") && line.length > 1) return line.slice(1);
15197
+ }
15397
15198
  } catch {
15398
15199
  }
15200
+ return null;
15399
15201
  }
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
- );
15409
- }
15410
- function loadFromDisk2(key) {
15411
- const path = runLogPath(key);
15412
- if (!(0, import_node_fs41.existsSync)(path)) return null;
15202
+ function realpathOrResolve(path) {
15413
15203
  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
- };
15204
+ return (0, import_node_fs40.realpathSync)(path);
15421
15205
  } catch {
15422
- return null;
15206
+ return (0, import_node_path40.resolve)(path);
15423
15207
  }
15424
15208
  }
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;
15209
+ function isOtherWorktreeListenerCwd(cwd, worktreePath) {
15210
+ const wt = realpathOrResolve(worktreePath);
15211
+ const c = realpathOrResolve(cwd);
15212
+ if (c === wt || c.startsWith(wt + import_node_path40.sep)) return false;
15213
+ const parent = (0, import_node_path40.dirname)(wt);
15214
+ const thisName = (0, import_node_path40.basename)(wt);
15215
+ if (!thisName || c === parent) return false;
15216
+ if (!c.startsWith(parent + import_node_path40.sep)) return false;
15217
+ const first = c.slice(parent.length + 1).split(import_node_path40.sep)[0];
15218
+ return Boolean(first) && first !== thisName;
15431
15219
  }
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;
15220
+ function signalPid(pid, signal) {
15221
+ try {
15222
+ process.kill(pid, signal);
15223
+ } catch {
15439
15224
  }
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
15225
  }
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);
15226
+ function isThisWorktreeListenerCwd(cwd, worktreePath) {
15227
+ const wt = realpathOrResolve(worktreePath);
15228
+ const c = realpathOrResolve(cwd);
15229
+ return c === wt || c.startsWith(wt + import_node_path40.sep);
15471
15230
  }
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
+ function killDetachedRunTree(pid, worktreePath) {
15232
+ if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
15233
+ const cwd = listenerCwd(pid);
15234
+ if (cwd) {
15235
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
15236
+ } else if (!processGroupAlive(pid)) {
15237
+ return;
15238
+ }
15239
+ try {
15240
+ process.kill(-pid, "SIGTERM");
15241
+ } catch {
15242
+ signalPid(pid, "SIGTERM");
15493
15243
  }
15494
- });
15495
-
15496
- // src/git/orphan-cleanup.ts
15497
- function isSideboardWorktreePath(path) {
15498
- return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
15499
15244
  }
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)) {
15245
+ function killListenersOnPorts(ports) {
15246
+ const selfPid = process.pid;
15247
+ for (const port of ports) {
15248
+ if (!Number.isFinite(port) || port <= 0) continue;
15508
15249
  try {
15509
- for (const entry of (0, import_node_fs42.readdirSync)(homeRoot, { withFileTypes: true })) {
15510
- if (!entry.isDirectory()) continue;
15511
- void entry;
15250
+ if (process.platform === "win32") {
15251
+ (0, import_node_child_process4.execFileSync)(
15252
+ "powershell",
15253
+ [
15254
+ "-NoProfile",
15255
+ "-Command",
15256
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
15257
+ ],
15258
+ { stdio: "ignore", timeout: 3e3 }
15259
+ );
15260
+ continue;
15261
+ }
15262
+ for (const pid of listUnixListenerPids(port, selfPid)) {
15263
+ signalPid(pid, "SIGTERM");
15512
15264
  }
15513
15265
  } catch {
15514
15266
  }
15515
15267
  }
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 {
15268
+ }
15269
+ function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
15270
+ if (!worktreePath.trim()) {
15271
+ killListenersOnPorts(ports);
15272
+ return;
15273
+ }
15274
+ if (process.platform === "win32") {
15275
+ killListenersOnPorts(ports);
15276
+ return;
15277
+ }
15278
+ const selfPid = process.pid;
15279
+ for (const port of ports) {
15280
+ if (!Number.isFinite(port) || port <= 0) continue;
15281
+ for (const pid of listUnixListenerPids(port, selfPid)) {
15282
+ const cwd = listenerCwd(pid);
15283
+ if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
15284
+ signalPid(pid, signal);
15537
15285
  }
15286
+ }
15287
+ }
15288
+ function killScriptTree(child, ports = []) {
15289
+ const pid = child.pid;
15290
+ if (pid) {
15538
15291
  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;
15292
+ if (process.platform === "win32") {
15293
+ void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
15294
+ } else {
15295
+ process.kill(-pid, "SIGTERM");
15296
+ setTimeout(() => {
15548
15297
  try {
15549
- mtimeMs = (0, import_node_fs42.statSync)(path).mtimeMs;
15298
+ process.kill(-pid, "SIGKILL");
15550
15299
  } catch {
15551
- mtimeMs = Date.now();
15552
15300
  }
15553
- orphans.push({ path, repoPath, mtimeMs });
15554
- }
15301
+ }, 2500).unref?.();
15555
15302
  }
15556
15303
  } catch {
15304
+ try {
15305
+ child.kill("SIGTERM");
15306
+ } catch {
15307
+ }
15557
15308
  }
15558
15309
  }
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;
15310
+ killListenersOnPorts(ports);
15600
15311
  }
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(
15312
+ async function spawnWorkspaceScript(command, opts) {
15313
+ const loginEnv = await captureLoginEnv();
15314
+ const env = buildWorkspaceScriptEnv(
15617
15315
  {
15618
- ...thread,
15619
- attachments: [],
15620
- pendingTurnAttachments: [],
15621
- queueAttachments: [],
15622
- messages: [historyRetentionStubMessage(thread)]
15316
+ worktreePath: opts.worktreePath,
15317
+ repoPath: opts.repoPath,
15318
+ workspaceName: opts.workspaceName,
15319
+ defaultBranch: opts.defaultBranch,
15320
+ ports: opts.ports
15623
15321
  },
15624
- { touch: false }
15322
+ loginEnv
15625
15323
  );
15626
- }
15627
- function cleanupArchivedHistory(opts = {}) {
15628
- const settings = loadAppSettings();
15629
- if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
15630
- return { stripped: [], purged: [] };
15324
+ try {
15325
+ mergeAgentGitAuthEnv(
15326
+ env,
15327
+ await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
15328
+ );
15329
+ } catch {
15631
15330
  }
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
15331
+ if (opts.heldPorts?.length) {
15332
+ await Promise.all(opts.heldPorts.map((h) => h.release()));
15333
+ }
15334
+ const shell = process.platform === "darwin" ? "zsh" : "bash";
15335
+ const child = (0, import_execa4.execa)(shell, ["-lc", command], {
15336
+ cwd: opts.worktreePath,
15337
+ reject: false,
15338
+ env,
15339
+ // Own process group so Stop can tear down the whole tree (not just the shell).
15340
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
15341
+ ...process.platform === "win32" ? {} : { detached: true }
15638
15342
  });
15639
- const stripped = [];
15640
- const purged = [];
15641
- if (opts.dryRun) {
15642
- return { stripped: plan.stripIds, purged: plan.purgeIds };
15343
+ pipeLines(child.stdout, opts.onLine);
15344
+ pipeLines(child.stderr, opts.onLine);
15345
+ const ports = opts.ports ?? [];
15346
+ return {
15347
+ pid: child.pid,
15348
+ kill: () => killScriptTree(child, ports),
15349
+ done: child.then((r) => r.exitCode ?? null),
15350
+ child
15351
+ };
15352
+ }
15353
+ async function attachAbort(handle, signal) {
15354
+ if (signal) {
15355
+ const onAbort = () => handle.kill();
15356
+ if (signal.aborted) onAbort();
15357
+ else signal.addEventListener("abort", onAbort, { once: true });
15643
15358
  }
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
- }
15359
+ return handle.done;
15360
+ }
15361
+ async function runSetupScript(repoPath, worktreePath, onLine, opts) {
15362
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15363
+ if (!settings?.setup) return { ran: false, exitCode: null, source: null };
15364
+ const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
15365
+ onLine?.(`[setup] ${source ?? "settings.toml"}`);
15366
+ const handle = await spawnWorkspaceScript(settings.setup, {
15367
+ worktreePath,
15368
+ repoPath,
15369
+ defaultBranch: opts?.defaultBranch,
15370
+ onLine
15371
+ });
15372
+ const exitCode = await attachAbort(handle, opts?.signal);
15373
+ return {
15374
+ ran: true,
15375
+ exitCode,
15376
+ source,
15377
+ kill: handle.kill
15378
+ };
15379
+ }
15380
+ async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
15381
+ const found = findConventionSetup(worktreePath, repoPath);
15382
+ if (!found) return { ran: false, exitCode: null, source: null };
15383
+ onLine?.(`[setup] ${found.source}`);
15384
+ const handle = await spawnWorkspaceScript(found.command, {
15385
+ worktreePath,
15386
+ repoPath,
15387
+ defaultBranch: opts?.defaultBranch,
15388
+ onLine
15389
+ });
15390
+ const exitCode = await attachAbort(handle, opts?.signal);
15391
+ return {
15392
+ ran: true,
15393
+ exitCode,
15394
+ source: found.source,
15395
+ kill: handle.kill
15396
+ };
15397
+ }
15398
+ async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
15399
+ await ensureWorktreeSideboardIgnored(worktreePath);
15400
+ const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
15401
+ if (review.wrote) {
15402
+ onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
15652
15403
  }
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
- }
15404
+ let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
15405
+ if (!setup.ran) {
15406
+ setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
15661
15407
  }
15662
- if (stripped.length > 0 || purged.length > 0) {
15663
- updateAdvancedSettings({
15664
- historyLastCleanupAt: new Date(nowMs).toISOString()
15665
- });
15408
+ if (!setup.ran) {
15409
+ setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
15666
15410
  }
15667
- return { stripped, purged };
15668
- }
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;
15411
+ return setup;
15676
15412
  }
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
15413
+ async function runArchiveScript(repoPath, worktreePath, onLine) {
15414
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15415
+ if (!settings?.archive) return { ran: false, exitCode: null };
15416
+ const handle = await spawnWorkspaceScript(settings.archive, {
15417
+ worktreePath,
15418
+ repoPath,
15419
+ onLine
15693
15420
  });
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
15421
+ const exitCode = await handle.done;
15422
+ return { ran: true, exitCode };
15423
+ }
15424
+ function listRunScripts(worktreePath, repoPath) {
15425
+ const settings = loadWorkspaceSettings(worktreePath, repoPath);
15426
+ if (!settings?.runScripts.length) return [];
15427
+ return settings.runScripts.filter((s) => {
15428
+ if (!s.availableIn?.length) return true;
15429
+ return s.availableIn.includes("local");
15702
15430
  });
15703
- if (checkout.exitCode !== 0) {
15704
- throw new Error(
15705
- `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
15706
- );
15431
+ }
15432
+ function getDefaultRunScript(worktreePath, repoPath) {
15433
+ const scripts = listRunScripts(worktreePath, repoPath);
15434
+ if (!scripts.length) return null;
15435
+ return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
15436
+ }
15437
+ function getRunScript(worktreePath, repoPath, name) {
15438
+ const scripts = listRunScripts(worktreePath, repoPath);
15439
+ if (!scripts.length) return null;
15440
+ if (name) {
15441
+ return scripts.find((s) => s.name === name) ?? null;
15707
15442
  }
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
- );
15443
+ return getDefaultRunScript(worktreePath, repoPath);
15444
+ }
15445
+ function getRunMode(worktreePath, repoPath) {
15446
+ return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
15447
+ }
15448
+ async function reservePort() {
15449
+ return new Promise((resolve2, reject) => {
15450
+ const server = (0, import_node_net.createServer)();
15451
+ let released = false;
15452
+ const release = () => new Promise((res) => {
15453
+ if (released) {
15454
+ res();
15455
+ return;
15736
15456
  }
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
15457
+ released = true;
15458
+ server.close(() => res());
15754
15459
  });
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
- };
15765
- }
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 };
15460
+ server.listen(0, "127.0.0.1", () => {
15461
+ const addr = server.address();
15462
+ if (!addr || typeof addr === "string") {
15463
+ void release().then(() => reject(new Error("Failed to allocate port")));
15464
+ return;
15465
+ }
15466
+ resolve2({ port: addr.port, release });
15467
+ });
15468
+ server.on("error", reject);
15469
+ });
15800
15470
  }
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();
15811
- }
15812
- });
15813
-
15814
- // src/types/thread.ts
15815
- function isAdoptableRunScriptRequest(req) {
15816
- return Boolean(req && !req.fulfilledAt && !req.error);
15471
+ async function tryReservePort(port) {
15472
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
15473
+ return new Promise((resolve2) => {
15474
+ const server = (0, import_node_net.createServer)();
15475
+ let released = false;
15476
+ const release = () => new Promise((res) => {
15477
+ if (released) {
15478
+ res();
15479
+ return;
15480
+ }
15481
+ released = true;
15482
+ server.close(() => res());
15483
+ });
15484
+ server.once("error", () => {
15485
+ try {
15486
+ server.close();
15487
+ } catch {
15488
+ }
15489
+ resolve2(null);
15490
+ });
15491
+ server.listen(port, "127.0.0.1", () => {
15492
+ resolve2({ port, release });
15493
+ });
15494
+ });
15817
15495
  }
15818
- var init_thread = __esm({
15819
- "src/types/thread.ts"() {
15820
- "use strict";
15496
+ function normalizePreferredPorts(ports, size) {
15497
+ if (!ports?.length) return [];
15498
+ const seen = /* @__PURE__ */ new Set();
15499
+ const out = [];
15500
+ for (const p of ports) {
15501
+ if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
15502
+ seen.add(p);
15503
+ out.push(p);
15504
+ if (out.length >= size) break;
15821
15505
  }
15822
- });
15823
-
15824
- // src/store/desktop-host.ts
15825
- function desktopHostPidPath() {
15826
- return (0, import_node_path44.join)(appDataDir(), "desktop-host.pid");
15506
+ return out;
15827
15507
  }
15828
- function readDesktopHostPid() {
15829
- 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;
15835
- }
15508
+ function delay(ms) {
15509
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
15836
15510
  }
15837
- function pidAlive(pid) {
15511
+ async function fillPortRange(held, size) {
15512
+ if (held.length >= size) return held;
15513
+ const base = held[0]?.port;
15514
+ const taken = new Set(held.map((h) => h.port));
15838
15515
  try {
15839
- process.kill(pid, 0);
15840
- return true;
15841
- } catch {
15842
- return false;
15516
+ let offset = 0;
15517
+ while (held.length < size) {
15518
+ const candidate = base != null ? base + offset : null;
15519
+ offset += 1;
15520
+ if (candidate != null && taken.has(candidate)) continue;
15521
+ const next = candidate != null ? await tryReservePort(candidate) : null;
15522
+ if (next) {
15523
+ held.push(next);
15524
+ taken.add(next.port);
15525
+ } else {
15526
+ const ephemeral = await reservePort();
15527
+ held.push(ephemeral);
15528
+ taken.add(ephemeral.port);
15529
+ }
15530
+ }
15531
+ return held;
15532
+ } catch (err) {
15533
+ await Promise.all(held.map((h) => h.release()));
15534
+ throw err;
15843
15535
  }
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 tryReserveAvailablePorts(ports) {
15538
+ const held = [];
15539
+ for (const port of ports) {
15540
+ const next = await tryReservePort(port);
15541
+ if (next) held.push(next);
15863
15542
  }
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;
15543
+ return held;
15882
15544
  }
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);
15545
+ async function reclaimPreferredPorts(ports, worktreePath) {
15546
+ const primary = ports[0];
15547
+ if (primary == null) return null;
15548
+ const takePrimary = async () => {
15549
+ const held2 = await tryReserveAvailablePorts(ports);
15550
+ if (held2.some((h) => h.port === primary)) return held2;
15551
+ await Promise.all(held2.map((h) => h.release()));
15552
+ return null;
15553
+ };
15554
+ let held = await takePrimary();
15555
+ if (held) return held;
15556
+ if (worktreePath) {
15557
+ killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
15558
+ } else {
15559
+ killListenersOnPorts(ports);
15560
+ }
15561
+ for (let i = 0; i < RECLAIM_TRIES; i++) {
15562
+ await delay(RECLAIM_RETRY_DELAY_MS);
15563
+ held = await takePrimary();
15564
+ if (held) return held;
15565
+ if (i === 2 && worktreePath) {
15566
+ killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
15567
+ }
15568
+ }
15569
+ return null;
15889
15570
  }
15890
- function noticeKey(childId, status) {
15891
- return `${childId}:${status}`;
15571
+ async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
15572
+ const preferred = normalizePreferredPorts(opts?.preferred, size);
15573
+ if (preferred.length) {
15574
+ const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
15575
+ if (reused) return fillPortRange(reused, size);
15576
+ }
15577
+ const held = [await reservePort()];
15578
+ return fillPortRange(held, size);
15892
15579
  }
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);
15580
+ async function startDevServer(repoPath, worktreePath, onLine, opts) {
15581
+ const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
15582
+ if (!script) return null;
15583
+ const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
15584
+ const held = await reservePortRange(PORT_RANGE_SIZE, {
15585
+ preferred: preferred ?? void 0,
15586
+ worktreePath
15902
15587
  });
15903
- return true;
15588
+ const ports = held.map((h) => h.port);
15589
+ saveWorktreeRunPorts(worktreePath, script.name, ports);
15590
+ try {
15591
+ const handle = await spawnWorkspaceScript(script.command, {
15592
+ worktreePath,
15593
+ repoPath,
15594
+ ports,
15595
+ heldPorts: held,
15596
+ defaultBranch: opts?.defaultBranch,
15597
+ onLine
15598
+ });
15599
+ return {
15600
+ pid: handle.pid,
15601
+ port: ports[0],
15602
+ ports,
15603
+ scriptName: script.name,
15604
+ kill: handle.kill,
15605
+ done: handle.done
15606
+ };
15607
+ } catch (err) {
15608
+ await Promise.all(held.map((h) => h.release()));
15609
+ throw err;
15610
+ }
15904
15611
  }
15905
- var HALT_STATUSES, notified;
15906
- var init_child_halt = __esm({
15907
- "src/orchestrator/child-halt.ts"() {
15612
+ 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;
15613
+ var init_conductor = __esm({
15614
+ "src/hook/conductor.ts"() {
15908
15615
  "use strict";
15909
- init_global_workspace();
15910
- init_thread_store();
15911
- HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
15912
- notified = /* @__PURE__ */ new Set();
15616
+ import_node_fs40 = require("fs");
15617
+ import_node_child_process4 = require("child_process");
15618
+ import_node_net = require("net");
15619
+ import_node_path40 = require("path");
15620
+ import_execa4 = require("execa");
15621
+ import_node_readline3 = require("readline");
15622
+ init_settings();
15623
+ init_nested_electron_env();
15624
+ init_worktree_pkg_cache();
15625
+ init_convention_setup();
15626
+ init_cursor_worktrees();
15627
+ init_git_auth_mode();
15628
+ init_request_review();
15629
+ init_worktree_exclude();
15630
+ init_worktree_run_ports();
15631
+ init_wait_for_job();
15632
+ init_worktree_run_ports();
15633
+ init_nested_electron_env();
15634
+ PORT_RANGE_SIZE = 10;
15635
+ RECLAIM_RETRY_DELAY_MS = 40;
15636
+ RECLAIM_TRIES = 8;
15637
+ cachedLoginEnv = null;
15913
15638
  }
15914
15639
  });
15915
15640
 
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);
15641
+ // src/store/setup-log.ts
15642
+ function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
15643
+ const next = prev ? `${prev}
15644
+ ${line}` : line;
15645
+ if (next.length <= max) return next;
15646
+ const cut = next.slice(next.length - max);
15647
+ const nl = cut.indexOf("\n");
15648
+ return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
15921
15649
  }
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
- }
15650
+ function fromSnapshot(snap) {
15651
+ return {
15652
+ chunks: snap.output ? [snap.output] : [],
15653
+ length: snap.output.length,
15654
+ running: snap.running,
15655
+ exitCode: snap.exitCode,
15656
+ source: snap.source
15657
+ };
15937
15658
  }
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.";
15659
+ function materialize(mem) {
15660
+ let output = mem.chunks.join("\n");
15661
+ if (output.length > MAX_SETUP_LOG_CHARS) {
15662
+ output = appendSetupOutput("", output);
15663
+ mem.chunks = [output];
15664
+ mem.length = output.length;
15949
15665
  }
15950
- });
15951
-
15952
- // src/mcp/wait-for-job.ts
15953
- function mcpWaitForJobTimeoutMs(requested) {
15954
- return mcpWaitForTurnTimeoutMs(requested);
15666
+ return {
15667
+ output,
15668
+ running: mem.running,
15669
+ exitCode: mem.exitCode,
15670
+ source: mem.source
15671
+ };
15955
15672
  }
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;
15673
+ function setupLogPath(threadId) {
15674
+ return (0, import_node_path41.join)(threadsDir(), `${threadId}.setup.log.json`);
15962
15675
  }
15963
- function jobAlive(pid) {
15964
- if (!Number.isInteger(pid) || pid <= 0) return false;
15676
+ function setupLogKeyForWorktree(worktreePath) {
15677
+ const norm = normalizeWorktreePath(worktreePath);
15678
+ return `wt-${(0, import_node_crypto8.createHash)("sha1").update(norm).digest("hex").slice(0, 16)}`;
15679
+ }
15680
+ function emptySetupLog() {
15681
+ return { ...EMPTY };
15682
+ }
15683
+ function persistNow(threadId) {
15684
+ const timer = persistTimers.get(threadId);
15685
+ if (timer) {
15686
+ clearTimeout(timer);
15687
+ persistTimers.delete(threadId);
15688
+ }
15689
+ const mem = memory.get(threadId);
15690
+ if (!mem) return;
15965
15691
  try {
15966
- process.kill(pid, 0);
15967
- return true;
15692
+ (0, import_node_fs41.writeFileSync)(
15693
+ setupLogPath(threadId),
15694
+ `${JSON.stringify(materialize(mem))}
15695
+ `,
15696
+ "utf8"
15697
+ );
15968
15698
  } catch {
15969
- return false;
15970
15699
  }
15971
15700
  }
15972
- function processGroupAlive(pgid) {
15973
- if (process.platform === "win32") return false;
15974
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
15975
- try {
15976
- process.kill(-pgid, 0);
15977
- return true;
15978
- } catch (err) {
15979
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
15980
- }
15981
- }
15982
- function jobTreeAlive(pid) {
15983
- if (pid == null) return false;
15984
- return jobAlive(pid) || processGroupAlive(pid);
15985
- }
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;
15990
- }
15991
- function jobDir(root, id, legacy = false) {
15992
- return (0, import_node_path45.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
15993
- }
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;
16000
- }
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);
15701
+ function schedulePersist(threadId) {
15702
+ if (persistTimers.has(threadId)) return;
15703
+ persistTimers.set(
15704
+ threadId,
15705
+ setTimeout(() => {
15706
+ persistTimers.delete(threadId);
15707
+ persistNow(threadId);
15708
+ }, PERSIST_DEBOUNCE_MS)
15709
+ );
16005
15710
  }
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);
15711
+ function loadFromDisk(threadId) {
15712
+ const path = setupLogPath(threadId);
15713
+ if (!(0, import_node_fs41.existsSync)(path)) return null;
15714
+ try {
15715
+ const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
15716
+ return {
15717
+ output: typeof raw.output === "string" ? raw.output : "",
15718
+ running: Boolean(raw.running),
15719
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15720
+ source: typeof raw.source === "string" ? raw.source : null
15721
+ };
15722
+ } catch {
15723
+ return null;
16018
15724
  }
16019
- return running.sort();
16020
- }
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);
16027
- }
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");
16032
15725
  }
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");
15726
+ function memFor(threadId) {
15727
+ const cached = memory.get(threadId);
15728
+ if (cached) return cached;
15729
+ const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
15730
+ memory.set(threadId, mem);
15731
+ return mem;
16044
15732
  }
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)}`;
15733
+ function readSetupLog(threadId) {
15734
+ const cached = memory.get(threadId);
15735
+ if (cached) return materialize(cached);
15736
+ const disk = loadFromDisk(threadId);
15737
+ if (disk) {
15738
+ memory.set(threadId, fromSnapshot(disk));
15739
+ return disk;
16053
15740
  }
16054
- return out;
15741
+ return emptySetupLog();
16055
15742
  }
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(" ");
15743
+ function beginSetupLog(threadId) {
15744
+ const mem = {
15745
+ chunks: [],
15746
+ length: 0,
15747
+ running: true,
15748
+ exitCode: null,
15749
+ source: null
15750
+ };
15751
+ memory.set(threadId, mem);
15752
+ persistNow(threadId);
15753
+ return materialize(mem);
16064
15754
  }
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(" ");
15755
+ function appendSetupLog(threadId, line) {
15756
+ const mem = memFor(threadId);
15757
+ mem.chunks.push(line);
15758
+ mem.length += line.length + 1;
15759
+ mem.running = true;
15760
+ if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
15761
+ schedulePersist(threadId);
16071
15762
  }
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
- });
15763
+ function finishSetupLog(threadId, exitCode, source) {
15764
+ const mem = memFor(threadId);
15765
+ mem.running = false;
15766
+ mem.exitCode = exitCode;
15767
+ mem.source = source ?? mem.source;
15768
+ persistNow(threadId);
15769
+ return materialize(mem);
16079
15770
  }
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)) {
16087
- return {
16088
- action: "wait",
16089
- jobIds: opts.runningJobIds,
16090
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
15771
+ var import_node_crypto8, import_node_fs41, import_node_path41, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
15772
+ var init_setup_log = __esm({
15773
+ "src/store/setup-log.ts"() {
15774
+ "use strict";
15775
+ import_node_crypto8 = require("crypto");
15776
+ import_node_fs41 = require("fs");
15777
+ import_node_path41 = require("path");
15778
+ init_worktree_labels();
15779
+ init_paths();
15780
+ EMPTY = {
15781
+ output: "",
15782
+ running: false,
15783
+ exitCode: null,
15784
+ source: null
16091
15785
  };
15786
+ MAX_SETUP_LOG_CHARS = 256e3;
15787
+ memory = /* @__PURE__ */ new Map();
15788
+ persistTimers = /* @__PURE__ */ new Map();
15789
+ PERSIST_DEBOUNCE_MS = 500;
16092
15790
  }
16093
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
16094
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
15791
+ });
15792
+
15793
+ // src/store/run-log.ts
15794
+ function fromSnapshot2(snap) {
15795
+ return {
15796
+ chunks: snap.output ? [snap.output] : [],
15797
+ length: snap.output.length,
15798
+ running: snap.running,
15799
+ exitCode: snap.exitCode,
15800
+ scriptName: snap.scriptName
15801
+ };
15802
+ }
15803
+ function materialize2(mem) {
15804
+ let output = mem.chunks.join("\n");
15805
+ if (output.length > MAX_RUN_LOG_CHARS) {
15806
+ output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
15807
+ mem.chunks = [output];
15808
+ mem.length = output.length;
16095
15809
  }
16096
- return { action: "none" };
15810
+ return {
15811
+ output,
15812
+ running: mem.running,
15813
+ exitCode: mem.exitCode,
15814
+ scriptName: mem.scriptName
15815
+ };
16097
15816
  }
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");
15817
+ function safeScriptToken(scriptName) {
15818
+ const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
15819
+ return token || "dev";
16102
15820
  }
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;
15821
+ function runLogPath(key) {
15822
+ return (0, import_node_path42.join)(threadsDir(), `${key}.run.log.json`);
16106
15823
  }
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;
15824
+ function runLogKeyForWorktree(worktreePath, scriptName) {
15825
+ const norm = normalizeWorktreePath(worktreePath);
15826
+ const hash = (0, import_node_crypto9.createHash)("sha1").update(norm).digest("hex").slice(0, 16);
15827
+ return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
16112
15828
  }
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 };
15829
+ function emptyRunLog() {
15830
+ return { ...EMPTY2 };
16118
15831
  }
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
- };
15832
+ function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
15833
+ const max = Math.max(1, tailChars);
15834
+ const outputChars = output.length;
15835
+ if (outputChars <= max) return { output, truncated: false, outputChars };
15836
+ const cut = output.slice(outputChars - max);
15837
+ const nl = cut.indexOf("\n");
15838
+ const text7 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
15839
+ return { output: text7, truncated: true, outputChars };
16130
15840
  }
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);
15841
+ function persistNow2(key) {
15842
+ const timer = persistTimers2.get(key);
15843
+ if (timer) {
15844
+ clearTimeout(timer);
15845
+ persistTimers2.delete(key);
15846
+ }
15847
+ const mem = memory2.get(key);
15848
+ if (!mem) return;
16136
15849
  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
- `);
15850
+ (0, import_node_fs42.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
15851
+ `, "utf8");
16140
15852
  } catch {
16141
15853
  }
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
15854
+ }
15855
+ function schedulePersist2(key) {
15856
+ if (persistTimers2.has(key)) return;
15857
+ persistTimers2.set(
15858
+ key,
15859
+ setTimeout(() => {
15860
+ persistTimers2.delete(key);
15861
+ persistNow2(key);
15862
+ }, PERSIST_DEBOUNCE_MS2)
15863
+ );
15864
+ }
15865
+ function loadFromDisk2(key) {
15866
+ const path = runLogPath(key);
15867
+ if (!(0, import_node_fs42.existsSync)(path)) return null;
15868
+ try {
15869
+ const raw = JSON.parse((0, import_node_fs42.readFileSync)(path, "utf8"));
15870
+ return {
15871
+ output: typeof raw.output === "string" ? raw.output : "",
15872
+ running: Boolean(raw.running),
15873
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
15874
+ scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
15875
+ };
15876
+ } catch {
15877
+ return null;
15878
+ }
15879
+ }
15880
+ function memFor2(key) {
15881
+ const cached = memory2.get(key);
15882
+ if (cached) return cached;
15883
+ const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
15884
+ memory2.set(key, mem);
15885
+ return mem;
15886
+ }
15887
+ function readRunLog(key) {
15888
+ const cached = memory2.get(key);
15889
+ if (cached) return materialize2(cached);
15890
+ const disk = loadFromDisk2(key);
15891
+ if (disk) {
15892
+ memory2.set(key, fromSnapshot2(disk));
15893
+ return disk;
15894
+ }
15895
+ return emptyRunLog();
15896
+ }
15897
+ function beginRunLog(key, scriptName) {
15898
+ const mem = {
15899
+ chunks: [],
15900
+ length: 0,
15901
+ running: true,
15902
+ exitCode: null,
15903
+ scriptName
16156
15904
  };
15905
+ memory2.set(key, mem);
15906
+ persistNow2(key);
15907
+ return materialize2(mem);
16157
15908
  }
16158
- async function sleepMs(ms) {
16159
- await new Promise((resolve2) => setTimeout(resolve2, ms));
15909
+ function appendRunLog(key, line) {
15910
+ const mem = memFor2(key);
15911
+ mem.chunks.push(line);
15912
+ mem.length += line.length + 1;
15913
+ mem.running = true;
15914
+ if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
15915
+ schedulePersist2(key);
16160
15916
  }
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)) {
15917
+ function finishRunLog(key, exitCode) {
15918
+ const mem = memFor2(key);
15919
+ mem.running = false;
15920
+ mem.exitCode = exitCode;
15921
+ persistNow2(key);
15922
+ return materialize2(mem);
15923
+ }
15924
+ function flushRunLog(key) {
15925
+ persistNow2(key);
15926
+ }
15927
+ 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;
15928
+ var init_run_log = __esm({
15929
+ "src/store/run-log.ts"() {
15930
+ "use strict";
15931
+ import_node_crypto9 = require("crypto");
15932
+ import_node_fs42 = require("fs");
15933
+ import_node_path42 = require("path");
15934
+ init_worktree_labels();
15935
+ init_setup_log();
15936
+ init_paths();
15937
+ EMPTY2 = {
15938
+ output: "",
15939
+ running: false,
15940
+ exitCode: null,
15941
+ scriptName: null
15942
+ };
15943
+ MAX_RUN_LOG_CHARS = 256e3;
15944
+ DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
15945
+ memory2 = /* @__PURE__ */ new Map();
15946
+ persistTimers2 = /* @__PURE__ */ new Map();
15947
+ PERSIST_DEBOUNCE_MS2 = 500;
15948
+ }
15949
+ });
15950
+
15951
+ // src/git/orphan-cleanup.ts
15952
+ function isSideboardWorktreePath(path) {
15953
+ return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
15954
+ }
15955
+ async function findOrphanWorktrees(repoPaths) {
15956
+ const threads = listThreads({ includeArchived: true });
15957
+ const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
15958
+ const repos = new Set(
15959
+ repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
15960
+ );
15961
+ const homeRoot = sideboardWorkspacesDir();
15962
+ if ((0, import_node_fs43.existsSync)(homeRoot)) {
15963
+ try {
15964
+ for (const entry of (0, import_node_fs43.readdirSync)(homeRoot, { withFileTypes: true })) {
15965
+ if (!entry.isDirectory()) continue;
15966
+ void entry;
15967
+ }
15968
+ } catch {
15969
+ }
15970
+ }
15971
+ const orphans = [];
15972
+ const seen = /* @__PURE__ */ new Set();
15973
+ for (const repoPath of repos) {
15974
+ if (!repoPath || !(0, import_node_fs43.existsSync)(repoPath)) continue;
15975
+ try {
15976
+ const wts = await listWorktrees(repoPath);
15977
+ for (const wt of wts) {
15978
+ const path = wt.path.replace(/\/$/, "");
15979
+ if (!isSideboardWorktreePath(path)) continue;
15980
+ if (known.has(path)) continue;
15981
+ if (seen.has(path)) continue;
15982
+ seen.add(path);
15983
+ let mtimeMs = 0;
15984
+ try {
15985
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
15986
+ } catch {
15987
+ mtimeMs = 0;
15988
+ }
15989
+ orphans.push({ path, repoPath, mtimeMs });
15990
+ }
15991
+ } catch {
15992
+ }
15993
+ try {
15994
+ const root = worktreesRoot(repoPath);
15995
+ if ((0, import_node_fs43.existsSync)(root)) {
15996
+ for (const entry of (0, import_node_fs43.readdirSync)(root, { withFileTypes: true })) {
15997
+ if (!entry.isDirectory()) continue;
15998
+ const path = (0, import_node_path43.join)(root, entry.name).replace(/\/$/, "");
15999
+ if (known.has(path) || seen.has(path)) continue;
16000
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path43.join)(path, ".git"))) continue;
16001
+ seen.add(path);
16002
+ let mtimeMs = 0;
16003
+ try {
16004
+ mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
16005
+ } catch {
16006
+ mtimeMs = Date.now();
16007
+ }
16008
+ orphans.push({ path, repoPath, mtimeMs });
16009
+ }
16010
+ }
16011
+ } catch {
16012
+ }
16013
+ }
16014
+ return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
16015
+ }
16016
+ async function cleanupOrphanWorktrees(opts) {
16017
+ const settings = loadAppSettings();
16018
+ const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
16019
+ const orphans = await findOrphanWorktrees(opts?.repoPaths);
16020
+ const activeCount = listThreads().filter((t) => t.status !== "archived").length;
16021
+ const budget = Math.max(0, maxCount - activeCount);
16022
+ const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
16023
+ const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
16024
+ const removed = [];
16025
+ const kept = [];
16026
+ for (const orphan of orphans) {
16027
+ if (keep.has(orphan.path)) {
16028
+ kept.push(orphan.path);
16029
+ continue;
16030
+ }
16031
+ if (opts?.dryRun) {
16032
+ removed.push(orphan.path);
16033
+ continue;
16034
+ }
16035
+ try {
16036
+ await removeWorktree(orphan.repoPath, orphan.path);
16037
+ removed.push(orphan.path);
16038
+ } catch {
16039
+ kept.push(orphan.path);
16040
+ }
16041
+ }
16042
+ if (!opts?.dryRun) {
16043
+ updateAdvancedSettings({
16044
+ worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
16045
+ });
16046
+ }
16047
+ return { removed, kept, orphans };
16048
+ }
16049
+ function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
16050
+ const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
16051
+ const last = settings.advanced.worktreeLastCleanupAt;
16052
+ if (!last) return true;
16053
+ const elapsed = Date.now() - Date.parse(last);
16054
+ return elapsed >= intervalHours * 36e5;
16055
+ }
16056
+ var import_node_fs43, import_node_path43;
16057
+ var init_orphan_cleanup = __esm({
16058
+ "src/git/orphan-cleanup.ts"() {
16059
+ "use strict";
16060
+ import_node_fs43 = require("fs");
16061
+ import_node_path43 = require("path");
16062
+ init_worktree();
16063
+ init_thread_store();
16064
+ init_paths();
16065
+ init_app_settings();
16066
+ }
16067
+ });
16068
+
16069
+ // src/store/history-cleanup.ts
16070
+ function stripArchivedTranscript(thread) {
16071
+ writeThread(
16072
+ {
16073
+ ...thread,
16074
+ attachments: [],
16075
+ pendingTurnAttachments: [],
16076
+ queueAttachments: [],
16077
+ compactedMessages: void 0,
16078
+ messages: [historyRetentionStubMessage(thread)]
16079
+ },
16080
+ { touch: false }
16081
+ );
16082
+ }
16083
+ function cleanupArchivedHistory(opts = {}) {
16084
+ const settings = loadAppSettings();
16085
+ if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
16086
+ return { stripped: [], purged: [] };
16087
+ }
16088
+ const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
16089
+ const nowMs = opts.nowMs ?? Date.now();
16090
+ const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
16091
+ maxCount: opts.maxCount ?? historyMaxCount(settings),
16092
+ maxDays: opts.maxDays ?? historyMaxDays(settings),
16093
+ nowMs
16094
+ });
16095
+ const stripped = [];
16096
+ const purged = [];
16097
+ if (opts.dryRun) {
16098
+ return { stripped: plan.stripIds, purged: plan.purgeIds };
16099
+ }
16100
+ for (const id of plan.stripIds) {
16101
+ const thread = readThread(id);
16102
+ if (!thread || thread.status !== "archived") continue;
16103
+ try {
16104
+ stripArchivedTranscript(thread);
16105
+ stripped.push(id);
16106
+ } catch {
16107
+ }
16108
+ }
16109
+ for (const id of plan.purgeIds) {
16110
+ const thread = readThread(id);
16111
+ if (!thread || thread.status !== "archived") continue;
16112
+ try {
16113
+ deleteThreadRecord(id);
16114
+ purged.push(id);
16115
+ } catch {
16116
+ }
16117
+ }
16118
+ if (stripped.length > 0 || purged.length > 0) {
16119
+ updateAdvancedSettings({
16120
+ historyLastCleanupAt: new Date(nowMs).toISOString()
16121
+ });
16122
+ }
16123
+ return { stripped, purged };
16124
+ }
16125
+ function shouldRunHistoryCleanup(settings = loadAppSettings()) {
16126
+ if (!autoCleanupHistoryEnabled(settings)) return false;
16127
+ const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
16128
+ const last = settings.advanced.historyLastCleanupAt;
16129
+ if (!last) return true;
16130
+ const elapsed = Date.now() - Date.parse(last);
16131
+ return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
16132
+ }
16133
+ var init_history_cleanup = __esm({
16134
+ "src/store/history-cleanup.ts"() {
16135
+ "use strict";
16136
+ init_app_settings();
16137
+ init_history_retention();
16138
+ init_thread_store();
16139
+ }
16140
+ });
16141
+
16142
+ // src/git/apply-into-main.ts
16143
+ async function applyThreadIntoMain(thread, opts) {
16144
+ const method = opts?.method ?? "merge";
16145
+ const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
16146
+ const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
16147
+ cwd: thread.repoPath,
16148
+ reject: false
16149
+ });
16150
+ if (status.stdout.trim()) {
16151
+ throw new Error(
16152
+ `Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
16153
+ );
16154
+ }
16155
+ const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
16156
+ cwd: thread.repoPath,
16157
+ reject: false
16158
+ });
16159
+ if (checkout.exitCode !== 0) {
16160
+ throw new Error(
16161
+ `Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
16162
+ );
16163
+ }
16164
+ if (method === "cherry-pick") {
16165
+ const log = await (0, import_execa5.execa)(
16166
+ "git",
16167
+ ["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
16168
+ { cwd: thread.repoPath, reject: false }
16169
+ );
16170
+ const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
16171
+ if (!shas.length) {
16172
+ return {
16173
+ applied: false,
16174
+ method,
16175
+ targetBranch: target,
16176
+ message: `No commits on ${thread.branchName} not already in ${target}`
16177
+ };
16178
+ }
16179
+ for (const sha of shas) {
16180
+ const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
16181
+ cwd: thread.repoPath,
16182
+ reject: false
16183
+ });
16184
+ if (cp.exitCode !== 0) {
16185
+ await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
16186
+ cwd: thread.repoPath,
16187
+ reject: false
16188
+ });
16189
+ throw new Error(
16190
+ `Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
16191
+ );
16192
+ }
16193
+ }
16168
16194
  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."
16195
+ applied: true,
16196
+ method,
16197
+ targetBranch: target,
16198
+ message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
16177
16199
  };
16178
16200
  }
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
- });
16201
+ const merge = await (0, import_execa5.execa)(
16202
+ "git",
16203
+ ["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
16204
+ { cwd: thread.repoPath, reject: false }
16205
+ );
16206
+ if (merge.exitCode !== 0) {
16207
+ await (0, import_execa5.execa)("git", ["merge", "--abort"], {
16208
+ cwd: thread.repoPath,
16209
+ reject: false
16210
+ });
16211
+ throw new Error(
16212
+ `Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
16213
+ );
16214
+ }
16215
+ return {
16216
+ applied: true,
16217
+ method: "merge",
16218
+ targetBranch: target,
16219
+ message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
16220
+ };
16221
+ }
16222
+ var import_execa5;
16223
+ var init_apply_into_main = __esm({
16224
+ "src/git/apply-into-main.ts"() {
16225
+ "use strict";
16226
+ import_execa5 = require("execa");
16227
+ init_worktree();
16228
+ }
16229
+ });
16230
+
16231
+ // src/git/clone-repo.ts
16232
+ async function cloneRepoIntoSideboard(opts) {
16233
+ const url = opts.url.trim();
16234
+ if (!url) throw new Error("Clone URL is required");
16235
+ let name = opts.name?.trim();
16236
+ if (!name) {
16237
+ const leaf = (0, import_node_path44.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
16238
+ name = leaf || "repo";
16187
16239
  }
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())));
16240
+ name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
16241
+ const dest = (0, import_node_path44.join)(sideboardReposDir(), name);
16242
+ if ((0, import_node_fs44.existsSync)(dest)) {
16243
+ const repoPath2 = await resolveRepoRoot(dest);
16244
+ const workspace2 = await ensureWorkspace(repoPath2);
16245
+ return { repoPath: repoPath2, workspace: workspace2 };
16193
16246
  }
16194
- snap = snapshotJob(dir);
16195
- return toResult(jobId, snap);
16247
+ const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
16248
+ if (result.exitCode !== 0) {
16249
+ throw new Error(
16250
+ `git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
16251
+ );
16252
+ }
16253
+ const repoPath = await resolveRepoRoot(dest);
16254
+ const workspace = await ensureWorkspace(repoPath);
16255
+ return { repoPath, workspace };
16196
16256
  }
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
- }
16257
+ var import_node_fs44, import_node_path44, import_execa6;
16258
+ var init_clone_repo = __esm({
16259
+ "src/git/clone-repo.ts"() {
16260
+ "use strict";
16261
+ import_node_fs44 = require("fs");
16262
+ import_node_path44 = require("path");
16263
+ import_execa6 = require("execa");
16264
+ init_paths();
16265
+ init_workspaces();
16266
+ init_worktree();
16267
+ }
16268
+ });
16269
+
16270
+ // src/types/thread.ts
16271
+ function isAdoptableRunScriptRequest(req) {
16272
+ return Boolean(req && !req.fulfilledAt && !req.error);
16273
+ }
16274
+ var init_thread = __esm({
16275
+ "src/types/thread.ts"() {
16276
+ "use strict";
16205
16277
  }
16278
+ });
16279
+
16280
+ // src/store/desktop-host.ts
16281
+ function desktopHostPidPath() {
16282
+ return (0, import_node_path45.join)(appDataDir(), "desktop-host.pid");
16283
+ }
16284
+ function readDesktopHostPid() {
16206
16285
  try {
16207
- process.kill(pid, signal);
16286
+ const pid = Number.parseInt((0, import_node_fs45.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
16287
+ if (!Number.isFinite(pid) || pid <= 0) return null;
16288
+ return pid;
16208
16289
  } catch {
16290
+ return null;
16209
16291
  }
16210
16292
  }
16211
- function appendJobLog(logFile, line) {
16293
+ function pidAlive(pid) {
16212
16294
  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);
16295
+ process.kill(pid, 0);
16296
+ return true;
16217
16297
  } catch {
16298
+ return false;
16218
16299
  }
16219
16300
  }
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
- };
16301
+ function isDesktopHostAlive() {
16302
+ const pid = readDesktopHostPid();
16303
+ return pid != null && pidAlive(pid);
16284
16304
  }
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"() {
16305
+ function isThisProcessDesktopHost() {
16306
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
16307
+ }
16308
+ function thisProcessShouldDrainAgentQueues() {
16309
+ if (isThisProcessDesktopHost()) return true;
16310
+ return !isDesktopHostAlive();
16311
+ }
16312
+ var import_node_fs45, import_node_path45;
16313
+ var init_desktop_host = __esm({
16314
+ "src/store/desktop-host.ts"() {
16288
16315
  "use strict";
16289
16316
  import_node_fs45 = require("fs");
16290
16317
  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}$/;
16318
+ init_paths();
16319
+ }
16320
+ });
16321
+
16322
+ // src/orchestrator/child-halt.ts
16323
+ function isIncompleteChildStatus(status) {
16324
+ return HALT_STATUSES.has(status);
16325
+ }
16326
+ function childHaltNotice(child, status) {
16327
+ const title = child.title?.trim() || "Untitled";
16328
+ const link = `[${title}](sideboard://thread/${child.id})`;
16329
+ const why = child.lastError?.trim();
16330
+ const extra = why ? ` lastError: ${why}` : "";
16331
+ return [
16332
+ `Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
16333
+ "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."
16334
+ ].join("\n");
16335
+ }
16336
+ function shouldNotifyParentAfterTurnError(opts) {
16337
+ return !opts.quotaFailoverHandled && !opts.crashContinued;
16338
+ }
16339
+ function shouldNotifyParentOfChildHalt(opts) {
16340
+ if (!isIncompleteChildStatus(opts.status)) return false;
16341
+ if (!opts.child.parentThreadId) return false;
16342
+ if (!opts.parent || opts.parent.status === "archived") return false;
16343
+ if (opts.parent.id === opts.child.id) return false;
16344
+ return isOrchestratorThread(opts.parent);
16345
+ }
16346
+ function noticeKey(childId, status) {
16347
+ return `${childId}:${status}`;
16348
+ }
16349
+ function notifyParentOfChildHalt(child, status, send) {
16350
+ const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
16351
+ if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
16352
+ const key = noticeKey(child.id, status);
16353
+ if (notified.has(key)) return false;
16354
+ notified.add(key);
16355
+ const parentId = parent.id;
16356
+ void send(parentId, childHaltNotice(child, status)).catch(() => {
16357
+ notified.delete(key);
16358
+ });
16359
+ return true;
16360
+ }
16361
+ var HALT_STATUSES, notified;
16362
+ var init_child_halt = __esm({
16363
+ "src/orchestrator/child-halt.ts"() {
16364
+ "use strict";
16365
+ init_global_workspace();
16366
+ init_thread_store();
16367
+ HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
16368
+ notified = /* @__PURE__ */ new Set();
16301
16369
  }
16302
16370
  });
16303
16371
 
@@ -21069,7 +21137,7 @@ function formatWorktreeDirective(thread, opts) {
21069
21137
  const slug = opts?.githubSlug ?? "<origin-owner/name>";
21070
21138
  const resolve2 = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
21071
21139
  lines.push(
21072
- `- Prefer a draft PR first: \`gh pr create --draft -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
21140
+ `- Prefer a draft PR first: \`gh pr create --draft --assignee @me -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Assign to the GitHub-connected user (\`--assignee @me\`) unless the user named someone else. Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
21073
21141
  );
21074
21142
  }
21075
21143
  lines.push(
@@ -21364,6 +21432,13 @@ function isPresentPlanToolName(name) {
21364
21432
  if (!name) return false;
21365
21433
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
21366
21434
  }
21435
+ function threadMayPresentPlan(thread) {
21436
+ return Boolean(thread?.planMode);
21437
+ }
21438
+ function shouldRegisterPresentPlan(thread) {
21439
+ if (thread == null) return true;
21440
+ return threadMayPresentPlan(thread);
21441
+ }
21367
21442
  function extractPresentedPlan(parts) {
21368
21443
  if (!parts?.length) return null;
21369
21444
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -21401,7 +21476,7 @@ function resolvePlanMarkdown(opts) {
21401
21476
  }
21402
21477
  return null;
21403
21478
  }
21404
- var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
21479
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
21405
21480
  var init_plan_present = __esm({
21406
21481
  "src/plan/plan-present.ts"() {
21407
21482
  "use strict";
@@ -21409,6 +21484,8 @@ var init_plan_present = __esm({
21409
21484
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
21410
21485
  PLAN_FILE_NAME = "plan.md";
21411
21486
  LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
21487
+ 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.";
21488
+ 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
21489
  }
21413
21490
  });
21414
21491
 
@@ -21418,11 +21495,15 @@ __export(plan_file_exports, {
21418
21495
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
21419
21496
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
21420
21497
  PLAN_FILE_REL: () => PLAN_FILE_REL,
21498
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
21499
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
21421
21500
  extractPresentedPlan: () => extractPresentedPlan,
21422
21501
  isPresentPlanToolName: () => isPresentPlanToolName,
21423
21502
  planFileAbs: () => planFileAbs,
21424
21503
  readPlanFile: () => readPlanFile,
21425
21504
  resolvePlanMarkdown: () => resolvePlanMarkdown,
21505
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
21506
+ threadMayPresentPlan: () => threadMayPresentPlan,
21426
21507
  writePlanFile: () => writePlanFile
21427
21508
  });
21428
21509
  function ensureAttachmentsGitignore(worktreePath) {
@@ -23090,7 +23171,8 @@ var init_orchestrator = __esm({
23090
23171
  if (compact.didCompact) {
23091
23172
  thread = updateThread(threadId, {
23092
23173
  messages: compact.thread.messages,
23093
- sessionId: compact.thread.sessionId
23174
+ sessionId: compact.thread.sessionId,
23175
+ compactedMessages: compact.thread.compactedMessages
23094
23176
  });
23095
23177
  this.emit({
23096
23178
  type: "context_compacted",
@@ -23104,9 +23186,6 @@ var init_orchestrator = __esm({
23104
23186
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
23105
23187
  thread = this.requireThread(threadId);
23106
23188
  }
23107
- if (this.stoppedTurns.has(threadId)) {
23108
- return;
23109
- }
23110
23189
  const promptText = typeof prompt === "string" ? prompt : "";
23111
23190
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
23112
23191
  const parked = takePendingTurnAttachments(thread);
@@ -23123,6 +23202,14 @@ var init_orchestrator = __esm({
23123
23202
  agent: thread.agent,
23124
23203
  orch: isOrchestratorThread(thread)
23125
23204
  });
23205
+ if (this.stoppedTurns.has(threadId)) {
23206
+ const stopped = writeLiveStatus(threadId, "stopped");
23207
+ if (stopped?.status === "stopped") {
23208
+ this.emit({ type: "status_changed", threadId, status: "stopped" });
23209
+ }
23210
+ this.emit({ type: "turn_finished", threadId, exitCode: 1 });
23211
+ return;
23212
+ }
23126
23213
  thread = this.requireThread(threadId);
23127
23214
  const gitPrompt = expandCanonicalGitRequest(promptText);
23128
23215
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -23181,7 +23268,7 @@ var init_orchestrator = __esm({
23181
23268
  ].filter(Boolean).join("\n\n") || void 0;
23182
23269
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
23183
23270
  const agentPrompt = [
23184
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
23271
+ planModeTurnInstruction(thread.planMode),
23185
23272
  orchestrationReminder,
23186
23273
  prGateDirective,
23187
23274
  injectedNoticeContext,
@@ -26609,6 +26696,7 @@ function mcpArchiveBlockedReason(thread) {
26609
26696
 
26610
26697
  // src/mcp/server.ts
26611
26698
  init_profile();
26699
+ init_plan_present();
26612
26700
 
26613
26701
  // src/mcp/run-script-ref.ts
26614
26702
  init_chat_tabs();
@@ -29539,31 +29627,40 @@ async function startMcpServer() {
29539
29627
  return mcpJson(payload);
29540
29628
  }
29541
29629
  );
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());
29630
+ const callingThreadId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
29631
+ const callingThread = callingThreadId ? orch.getThread(callingThreadId) : null;
29632
+ if (shouldRegisterPresentPlan(callingThread)) {
29633
+ server.tool(
29634
+ "present_plan",
29635
+ PRESENT_PLAN_TOOL_DESCRIPTION,
29636
+ {
29637
+ title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
29638
+ content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
29639
+ thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
29640
+ },
29641
+ async ({ title, content, thread_id }) => {
29642
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
29643
+ const ref = thread_id?.trim() || callingThreadId;
29644
+ const t = ref ? orch.getThread(ref) : null;
29645
+ if (t && !threadMayPresentPlan(t)) {
29646
+ return mcpJson(
29647
+ { ok: false, error: PRESENT_PLAN_REQUIRES_PLAN_MODE },
29648
+ true
29649
+ );
29650
+ }
29651
+ let root = process.cwd();
29555
29652
  if (t?.worktreePath?.trim()) root = t.worktreePath;
29653
+ const path = writePlanFile2(root, content);
29654
+ const payload = {
29655
+ ok: true,
29656
+ path,
29657
+ title: title?.trim() || "Plan",
29658
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
29659
+ };
29660
+ return mcpJson(payload);
29556
29661
  }
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
- );
29662
+ );
29663
+ }
29567
29664
  server.tool(
29568
29665
  "present_schema",
29569
29666
  "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.",
@@ -30171,7 +30268,7 @@ async function startMcpServer() {
30171
30268
  error: true,
30172
30269
  action,
30173
30270
  lastError: message,
30174
- hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft -R <origin> --body-file <short.md>. Keep the description short." : void 0
30271
+ hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft --assignee @me -R <origin> --body-file <short.md>. Keep the description short." : void 0
30175
30272
  },
30176
30273
  true
30177
30274
  );