@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
package/dist/index.cjs CHANGED
@@ -1240,6 +1240,7 @@ function archivedUpdatedMs(thread) {
1240
1240
  }
1241
1241
  function isHistoryRetentionStub(thread) {
1242
1242
  if ((thread.attachments?.length ?? 0) > 0) return false;
1243
+ if ((thread.compactedMessages?.length ?? 0) > 0) return false;
1243
1244
  const messages = thread.messages ?? [];
1244
1245
  if (messages.length === 0) return true;
1245
1246
  if (messages.length > 1) return false;
@@ -3323,7 +3324,8 @@ function normalizeThread(raw) {
3323
3324
  runScriptRequest: normalizeRunScriptRequest(raw.runScriptRequest),
3324
3325
  quotaResumeAt: raw.quotaResumeAt ?? null,
3325
3326
  quotaContinuedFromId: raw.quotaContinuedFromId ?? null,
3326
- archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0
3327
+ archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0,
3328
+ compactedMessages: Array.isArray(raw.compactedMessages) ? raw.compactedMessages : void 0
3327
3329
  };
3328
3330
  }
3329
3331
  function createEmptyThread(partial) {
@@ -4415,6 +4417,16 @@ function skillToolFromTextPart(part, id) {
4415
4417
  updatedAt: Date.now()
4416
4418
  };
4417
4419
  }
4420
+ function compactSummaryPart(text7, id = "compact-summary") {
4421
+ return compactToolPart(text7, id, { status: "done" });
4422
+ }
4423
+ function compactPartsForSummaryMessage(message) {
4424
+ const existing = (message.parts ?? []).filter(
4425
+ (p) => p.type === "tool" && isCompactToolName(p.name)
4426
+ );
4427
+ if (existing.length) return existing;
4428
+ return [compactSummaryPart(message.text ?? "")];
4429
+ }
4418
4430
  function compactToolPart(text7, id, opts) {
4419
4431
  const input = { trigger: opts?.trigger ?? "auto" };
4420
4432
  return withPartTimes(
@@ -8606,6 +8618,7 @@ __export(worktree_exports, {
8606
8618
  getPrForHeadBranch: () => getPrForHeadBranch,
8607
8619
  getPrMeta: () => getPrMeta,
8608
8620
  ghHeadRef: () => ghHeadRef,
8621
+ ghPrCreateAssigneeArgs: () => ghPrCreateAssigneeArgs,
8609
8622
  ghRepoSelectArgs: () => ghRepoSelectArgs,
8610
8623
  githubAgentGitEnv: () => githubAgentGitEnv,
8611
8624
  isDefaultishSourceRef: () => isDefaultishSourceRef,
@@ -9855,6 +9868,11 @@ async function mergePr(cwd, selector, opts) {
9855
9868
  }
9856
9869
  return { url, state: "MERGED" };
9857
9870
  }
9871
+ function ghPrCreateAssigneeArgs(assignee) {
9872
+ const raw = assignee?.trim();
9873
+ if (raw && /^(none|unassigned)$/i.test(raw)) return [];
9874
+ return ["--assignee", raw || "@me"];
9875
+ }
9858
9876
  async function createOrUpdatePr(worktreePath, opts) {
9859
9877
  await ensureGhPreferOrigin(worktreePath);
9860
9878
  const slug = await resolveGithubRepoSlug(worktreePath);
@@ -9929,6 +9947,7 @@ async function createOrUpdatePrWithBodyFile(worktreePath, opts) {
9929
9947
  headRef
9930
9948
  ];
9931
9949
  if (opts.draft) args2.push("--draft");
9950
+ args2.push(...ghPrCreateAssigneeArgs(opts.assignee));
9932
9951
  await gh(args2, worktreePath, { reject: false });
9933
9952
  const again = await gh(
9934
9953
  [...repoArgs, "pr", "view", branchOnly, "--json", "url", "--jq", ".url"],
@@ -9950,6 +9969,7 @@ async function createOrUpdatePrWithBodyFile(worktreePath, opts) {
9950
9969
  headRef
9951
9970
  ];
9952
9971
  if (opts.draft) args.push("--draft");
9972
+ args.push(...ghPrCreateAssigneeArgs(opts.assignee));
9953
9973
  const created = await gh(args, worktreePath, { reject: false });
9954
9974
  if (created.exitCode !== 0) {
9955
9975
  const raw = created.stderr.trim() || created.stdout.trim() || "gh pr create failed";
@@ -10272,7 +10292,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
10272
10292
  "- Examples:",
10273
10293
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
10274
10294
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
10275
- "- 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>`).",
10295
+ "- 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>`).",
10276
10296
  "- 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`.",
10277
10297
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
10278
10298
  ].join("\n");
@@ -10481,7 +10501,7 @@ var init_coordinator_prompt = __esm({
10481
10501
  '- 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.',
10482
10502
  "- 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.",
10483
10503
  '- 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.',
10484
- '- 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.',
10504
+ '- 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.',
10485
10505
  "Process guides:",
10486
10506
  "- 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.",
10487
10507
  "- 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.",
@@ -12360,6 +12380,10 @@ var init_node_launch = __esm({
12360
12380
  // src/agents/injected-mcp.ts
12361
12381
  function sideboardWorktreeAllowedTools(opts) {
12362
12382
  const out = [...SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS];
12383
+ if (!opts?.planMode) {
12384
+ const idx = out.indexOf("mcp__sideboard__present_plan");
12385
+ if (idx >= 0) out.splice(idx, 1);
12386
+ }
12363
12387
  if (opts?.github !== false) out.push(...SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS);
12364
12388
  if (opts?.linear) out.push(...SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS);
12365
12389
  if (opts?.abletime) out.push(...SIDEBOARD_ABLETIME_MCP_ALLOWED_TOOLS);
@@ -12906,6 +12930,9 @@ var init_orch_mcp_isolation = __esm({
12906
12930
  });
12907
12931
 
12908
12932
  // src/agents/types.ts
12933
+ function planModeTurnInstruction(planMode) {
12934
+ return planMode ? PLAN_MODE_INSTRUCTION : PLAN_MODE_OFF_INSTRUCTION;
12935
+ }
12909
12936
  function permissionMode(thread) {
12910
12937
  if (thread.sourceType === "orchestration") {
12911
12938
  return {
@@ -12944,11 +12971,12 @@ function permissionMode(thread) {
12944
12971
  codexSandbox: "workspace-write"
12945
12972
  };
12946
12973
  }
12947
- var PLAN_MODE_INSTRUCTION;
12974
+ var PLAN_MODE_INSTRUCTION, PLAN_MODE_OFF_INSTRUCTION;
12948
12975
  var init_types = __esm({
12949
12976
  "src/agents/types.ts"() {
12950
12977
  "use strict";
12951
12978
  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.";
12979
+ PLAN_MODE_OFF_INSTRUCTION = "Plan mode is off. Do not call present_plan or ExitPlanMode \u2014 those only work when the user turns Plan on in the composer. If you need to propose an approach, write it in chat.";
12952
12980
  }
12953
12981
  });
12954
12982
 
@@ -13461,7 +13489,8 @@ var init_claude = __esm({
13461
13489
  ...sideboardWorktreeAllowedTools({
13462
13490
  github: true,
13463
13491
  linear: isLinearConnected(),
13464
- abletime: isAbleTimeConnected()
13492
+ abletime: isAbleTimeConnected(),
13493
+ planMode: thread.planMode
13465
13494
  }),
13466
13495
  ...brightsyMcpAllowedTools(injectedBrightsyNames)
13467
13496
  ];
@@ -15879,6 +15908,7 @@ __export(agents_exports, {
15879
15908
  CURSOR_SDK_INSTALL_HINT: () => CURSOR_SDK_INSTALL_HINT,
15880
15909
  ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
15881
15910
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
15911
+ PLAN_MODE_OFF_INSTRUCTION: () => PLAN_MODE_OFF_INSTRUCTION,
15882
15912
  allAdapters: () => allAdapters,
15883
15913
  assertOrchestratorCapableAgent: () => assertOrchestratorCapableAgent,
15884
15914
  brightsyAdapter: () => brightsyAdapter,
@@ -15929,6 +15959,7 @@ __export(agents_exports, {
15929
15959
  parseCursorRunnerLine: () => parseCursorRunnerLine,
15930
15960
  parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
15931
15961
  permissionMode: () => permissionMode,
15962
+ planModeTurnInstruction: () => planModeTurnInstruction,
15932
15963
  posixShellSingleQuote: () => posixShellSingleQuote,
15933
15964
  preferredCursorCostCents: () => preferredCursorCostCents,
15934
15965
  prepareTerminalCommand: () => prepareTerminalCommand,
@@ -16391,9 +16422,9 @@ function gitActionExpansion(action, base) {
16391
16422
  case "commit-push":
16392
16423
  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.";
16393
16424
  case "create-draft":
16394
- 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.";
16425
+ 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.";
16395
16426
  case "create-web":
16396
- return "Commit, push, then `gh pr create --web -R <origin-owner/name>`.";
16427
+ 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.";
16397
16428
  case "resolve-conflicts":
16398
16429
  return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
16399
16430
  case "ready-for-review":
@@ -16911,12 +16942,38 @@ ${match.text}`, "");
16911
16942
  blocks.push("Continue from this context. Do not repeat the summary unless asked.");
16912
16943
  return blocks.join("\n");
16913
16944
  }
16945
+ function compactArchiveProse(message) {
16946
+ if (message.origin === "continue") return null;
16947
+ if (message.role === "summary") return null;
16948
+ let text7 = (message.text ?? "").trim();
16949
+ if (!text7 && message.role === "agent") {
16950
+ text7 = lastAssistantMessageText(message.parts).trim();
16951
+ }
16952
+ if (!text7 || isInternalAgentStatusText(text7)) return null;
16953
+ return { role: message.role, text: text7, ts: message.ts };
16954
+ }
16955
+ function messagesForCompactArchive(older) {
16956
+ const out = [];
16957
+ for (const message of older) {
16958
+ const prose = compactArchiveProse(message);
16959
+ if (prose) out.push(prose);
16960
+ }
16961
+ return out;
16962
+ }
16963
+ function appendCompactedMessages(existing, older) {
16964
+ const next = messagesForCompactArchive(older);
16965
+ if (!existing?.length) return next;
16966
+ if (!next.length) return existing;
16967
+ return [...existing, ...next];
16968
+ }
16914
16969
  function applyCompaction(messages, summaryText, thresholds = {}) {
16915
16970
  const { older, recent } = splitForCompaction(messages, thresholds);
16916
16971
  if (older.length === 0) return messages;
16972
+ const text7 = summaryText.trim();
16917
16973
  const summary = {
16918
16974
  role: "summary",
16919
- text: summaryText.trim(),
16975
+ text: text7,
16976
+ parts: [compactSummaryPart(text7)],
16920
16977
  ts: (/* @__PURE__ */ new Date()).toISOString()
16921
16978
  };
16922
16979
  return [summary, ...recent];
@@ -16944,6 +17001,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
16944
17001
  cwd: thread.worktreePath
16945
17002
  });
16946
17003
  let messages = applyCompaction(thread.messages, summary, thresholds);
17004
+ const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
16947
17005
  const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
16948
17006
  if (resetSession) {
16949
17007
  messages = applyForwardOccupancy(messages);
@@ -16951,6 +17009,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
16951
17009
  const next = {
16952
17010
  ...thread,
16953
17011
  messages,
17012
+ ...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
16954
17013
  sessionId: resetSession ? null : thread.sessionId,
16955
17014
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16956
17015
  };
@@ -16966,6 +17025,7 @@ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAG
16966
17025
  var init_context_compact = __esm({
16967
17026
  "src/composer/context-compact.ts"() {
16968
17027
  "use strict";
17028
+ init_message_parts();
16969
17029
  init_error_detail();
16970
17030
  init_usage();
16971
17031
  init_context_estimate();
@@ -17552,15 +17612,403 @@ var init_worktree_run_ports = __esm({
17552
17612
  }
17553
17613
  });
17554
17614
 
17615
+ // src/mcp/wait-for-turn.ts
17616
+ function mcpWaitForTurnTimeoutMs(requested) {
17617
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
17618
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
17619
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
17620
+ }
17621
+ function mcpWaitTaskHint(taskState, status) {
17622
+ switch (taskState) {
17623
+ case "submitted":
17624
+ return MCP_WAIT_QUEUED_HINT;
17625
+ case "working":
17626
+ return MCP_WAIT_STILL_RUNNING_HINT;
17627
+ case "input-required":
17628
+ return MCP_WAIT_INPUT_REQUIRED_HINT;
17629
+ case "canceled":
17630
+ return MCP_WAIT_STOPPED_HINT;
17631
+ case "failed":
17632
+ return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
17633
+ default:
17634
+ return void 0;
17635
+ }
17636
+ }
17637
+ 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;
17638
+ var init_wait_for_turn = __esm({
17639
+ "src/mcp/wait-for-turn.ts"() {
17640
+ "use strict";
17641
+ MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
17642
+ 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.";
17643
+ 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.";
17644
+ 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.";
17645
+ MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
17646
+ 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.";
17647
+ 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.";
17648
+ }
17649
+ });
17650
+
17651
+ // src/mcp/wait-for-job.ts
17652
+ function mcpWaitForJobTimeoutMs(requested) {
17653
+ return mcpWaitForTurnTimeoutMs(requested);
17654
+ }
17655
+ function sanitizeDetachedJobId(id) {
17656
+ const s = id.trim();
17657
+ if (!JOB_ID_RE.test(s)) {
17658
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
17659
+ }
17660
+ return s;
17661
+ }
17662
+ function jobAlive(pid) {
17663
+ if (!Number.isInteger(pid) || pid <= 0) return false;
17664
+ try {
17665
+ process.kill(pid, 0);
17666
+ return true;
17667
+ } catch {
17668
+ return false;
17669
+ }
17670
+ }
17671
+ function processGroupAlive(pgid) {
17672
+ if (process.platform === "win32") return false;
17673
+ if (!Number.isInteger(pgid) || pgid <= 1) return false;
17674
+ try {
17675
+ process.kill(-pgid, 0);
17676
+ return true;
17677
+ } catch (err) {
17678
+ return err && typeof err === "object" && "code" in err && err.code === "EPERM";
17679
+ }
17680
+ }
17681
+ function jobTreeAlive(pid) {
17682
+ if (pid == null) return false;
17683
+ return jobAlive(pid) || processGroupAlive(pid);
17684
+ }
17685
+ function readIntFile(file) {
17686
+ if (!(0, import_node_fs45.existsSync)(file)) return null;
17687
+ const n = Number.parseInt((0, import_node_fs45.readFileSync)(file, "utf8").trim(), 10);
17688
+ return Number.isInteger(n) ? n : null;
17689
+ }
17690
+ function jobDir(root, id, legacy = false) {
17691
+ return (0, import_node_path45.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
17692
+ }
17693
+ function resolveJobDir(root, id) {
17694
+ const modern = jobDir(root, id, false);
17695
+ if ((0, import_node_fs45.existsSync)(modern)) return modern;
17696
+ const legacy = jobDir(root, id, true);
17697
+ if ((0, import_node_fs45.existsSync)(legacy)) return legacy;
17698
+ return modern;
17699
+ }
17700
+ function listJobIdsIn(root, rel) {
17701
+ const dir = (0, import_node_path45.join)(root, rel);
17702
+ if (!(0, import_node_fs45.existsSync)(dir)) return [];
17703
+ return (0, import_node_fs45.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
17704
+ }
17705
+ function listRunningDetachedJobs(worktreePath) {
17706
+ const root = worktreePath.trim();
17707
+ if (!root) return [];
17708
+ const ids = /* @__PURE__ */ new Set([
17709
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
17710
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
17711
+ ]);
17712
+ const running = [];
17713
+ for (const id of ids) {
17714
+ const dir = resolveJobDir(root, id);
17715
+ const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
17716
+ if (jobTreeAlive(pid)) running.push(id);
17717
+ }
17718
+ return running.sort();
17719
+ }
17720
+ function looksLikeDeferredDonePromise(text7) {
17721
+ const t = (text7 ?? "").trim();
17722
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
17723
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
17724
+ if (!unquoted) return false;
17725
+ 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);
17726
+ }
17727
+ function isJobContinuePrompt(text7) {
17728
+ if (typeof text7 !== "string") return false;
17729
+ const t = text7.trim();
17730
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
17731
+ }
17732
+ function collapseGhRunWatchLog(text7) {
17733
+ if (!text7.includes(GH_WATCH_REFRESH)) return text7;
17734
+ const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
17735
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
17736
+ if (watchFrames.length <= 1) return text7;
17737
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
17738
+ const last = watchFrames[watchFrames.length - 1] ?? "";
17739
+ const omitted = watchFrames.length - 1;
17740
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
17741
+ `;
17742
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
17743
+ }
17744
+ function capJobLogDelta(text7) {
17745
+ const collapsed = collapseGhRunWatchLog(text7);
17746
+ const lines = collapsed.split("\n");
17747
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
17748
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
17749
+ if (out.length > MAX_JOB_DELTA_CHARS) {
17750
+ out = `\u2026(truncated ${out.length} chars)
17751
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
17752
+ }
17753
+ return out;
17754
+ }
17755
+ function formatJobStillRunningContinuePrompt(jobIds) {
17756
+ const ids = jobIds.join(", ");
17757
+ return [
17758
+ `Detached job still running: ${ids}.`,
17759
+ "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
17760
+ "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
17761
+ "Then report the result. Do not tell the user you will let them know later."
17762
+ ].join(" ");
17763
+ }
17764
+ function formatDeferredDoneContinuePrompt() {
17765
+ return [
17766
+ "You ended the turn after promising to report later, but no detached job is running.",
17767
+ "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).",
17768
+ "Do not say you will let the user know later."
17769
+ ].join(" ");
17770
+ }
17771
+ function turnWatchedDetachedJob(parts) {
17772
+ return parts.some((p) => {
17773
+ if (p.type !== "tool") return false;
17774
+ if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
17775
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
17776
+ return /detached-job\.(?:js|cjs)\b/i.test(blob);
17777
+ });
17778
+ }
17779
+ function planJobContinue(opts) {
17780
+ if (opts.isOrchestrator) return { action: "none" };
17781
+ if (opts.agent === "brightsy") return { action: "none" };
17782
+ if (opts.queueLength > 0) return { action: "none" };
17783
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
17784
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
17785
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
17786
+ return {
17787
+ action: "wait",
17788
+ jobIds: opts.runningJobIds,
17789
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
17790
+ };
17791
+ }
17792
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
17793
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
17794
+ }
17795
+ return { action: "none" };
17796
+ }
17797
+ function tailProgress(logFile, maxLines = 12) {
17798
+ if (!(0, import_node_fs45.existsSync)(logFile)) return "(no log yet)";
17799
+ const lines = (0, import_node_fs45.readFileSync)(logFile, "utf8").split("\n");
17800
+ return lines.slice(-maxLines).join("\n");
17801
+ }
17802
+ function lastProgressLine(progress) {
17803
+ const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
17804
+ return last ? last.slice(0, 72) : void 0;
17805
+ }
17806
+ function readLogLines(file) {
17807
+ if (!(0, import_node_fs45.existsSync)(file)) return [];
17808
+ const lines = (0, import_node_fs45.readFileSync)(file, "utf8").split("\n");
17809
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
17810
+ return lines;
17811
+ }
17812
+ function takeDelta(logFile, cursorFile) {
17813
+ const lines = readLogLines(logFile);
17814
+ const cursor = readIntFile(cursorFile) ?? 0;
17815
+ const start = Math.min(Math.max(0, cursor), lines.length);
17816
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
17817
+ }
17818
+ function snapshotJob(dir) {
17819
+ const pid = readIntFile((0, import_node_path45.join)(dir, "pid"));
17820
+ const running = jobTreeAlive(pid);
17821
+ return {
17822
+ pid,
17823
+ running,
17824
+ exitCode: readIntFile((0, import_node_path45.join)(dir, "exit")),
17825
+ log: (0, import_node_path45.join)(dir, "log"),
17826
+ cursor: (0, import_node_path45.join)(dir, "present.cursor"),
17827
+ progress: tailProgress((0, import_node_path45.join)(dir, "log"))
17828
+ };
17829
+ }
17830
+ function toResult(id, snap, extra) {
17831
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
17832
+ const ok = !snap.running && snap.exitCode === 0;
17833
+ const stillRunning = snap.running && !ok;
17834
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
17835
+ try {
17836
+ (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(snap.cursor), { recursive: true });
17837
+ (0, import_node_fs45.writeFileSync)(snap.cursor, `${nextCursor}
17838
+ `);
17839
+ } catch {
17840
+ }
17841
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
17842
+ const progress = extra?.progress ?? snap.progress;
17843
+ return {
17844
+ stillRunning,
17845
+ ok,
17846
+ failed: Boolean(failed && !stillRunning && !ok),
17847
+ status,
17848
+ id,
17849
+ pid: snap.pid,
17850
+ exitCode: snap.exitCode ?? void 0,
17851
+ phase: lastProgressLine(progress),
17852
+ delta,
17853
+ progress,
17854
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
17855
+ };
17856
+ }
17857
+ async function sleepMs(ms) {
17858
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
17859
+ }
17860
+ async function waitForDetachedJob(cwd, id, opts) {
17861
+ const jobId = sanitizeDetachedJobId(id);
17862
+ const root = cwd.trim() || process.cwd();
17863
+ const dir = resolveJobDir(root, jobId);
17864
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
17865
+ const sleep2 = opts?.sleep ?? sleepMs;
17866
+ if (!(0, import_node_fs45.existsSync)(dir)) {
17867
+ return {
17868
+ stillRunning: false,
17869
+ ok: false,
17870
+ failed: true,
17871
+ status: "failed",
17872
+ id: jobId,
17873
+ delta: "",
17874
+ progress: "No detached job. Start one first.",
17875
+ hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
17876
+ };
17877
+ }
17878
+ const deadline = Date.now() + timeoutMs;
17879
+ let snap = snapshotJob(dir);
17880
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
17881
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
17882
+ return toResult(jobId, snap, {
17883
+ failed: true,
17884
+ progress: "No detached job. Start one first."
17885
+ });
17886
+ }
17887
+ while (Date.now() < deadline) {
17888
+ snap = snapshotJob(dir);
17889
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
17890
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
17891
+ await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
17892
+ }
17893
+ snap = snapshotJob(dir);
17894
+ return toResult(jobId, snap);
17895
+ }
17896
+ function killProcessTree(pid, signal) {
17897
+ if (!Number.isInteger(pid) || pid <= 0) return;
17898
+ if (process.platform !== "win32") {
17899
+ try {
17900
+ process.kill(-pid, signal);
17901
+ return;
17902
+ } catch {
17903
+ }
17904
+ }
17905
+ try {
17906
+ process.kill(pid, signal);
17907
+ } catch {
17908
+ }
17909
+ }
17910
+ function appendJobLog(logFile, line) {
17911
+ try {
17912
+ (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(logFile), { recursive: true });
17913
+ const body = line.endsWith("\n") ? line : `${line}
17914
+ `;
17915
+ (0, import_node_fs45.appendFileSync)(logFile, body);
17916
+ } catch {
17917
+ }
17918
+ }
17919
+ async function stopDetachedJob(cwd, id, opts) {
17920
+ const jobId = sanitizeDetachedJobId(id);
17921
+ const root = cwd.trim() || process.cwd();
17922
+ const dir = resolveJobDir(root, jobId);
17923
+ const sleep2 = opts?.sleep ?? sleepMs;
17924
+ const why = opts?.reason?.trim() ?? "";
17925
+ if (!(0, import_node_fs45.existsSync)(dir)) {
17926
+ return {
17927
+ stillRunning: false,
17928
+ ok: false,
17929
+ failed: true,
17930
+ status: "failed",
17931
+ id: jobId,
17932
+ delta: "",
17933
+ progress: "No detached job. Start one first.",
17934
+ hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
17935
+ stopped: false,
17936
+ reason: "not-found"
17937
+ };
17938
+ }
17939
+ let snap = snapshotJob(dir);
17940
+ if (!snap.running) {
17941
+ const result2 = toResult(jobId, snap);
17942
+ return {
17943
+ ...result2,
17944
+ stopped: false,
17945
+ reason: "not-running",
17946
+ hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
17947
+ };
17948
+ }
17949
+ appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
17950
+ const targetPid = snap.pid;
17951
+ if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
17952
+ const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
17953
+ const deadline = Date.now() + graceMs;
17954
+ while (Date.now() < deadline && jobTreeAlive(targetPid)) {
17955
+ await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
17956
+ }
17957
+ if (targetPid != null) {
17958
+ killProcessTree(targetPid, "SIGKILL");
17959
+ await sleep2(100);
17960
+ }
17961
+ const exitFile = (0, import_node_path45.join)(dir, "exit");
17962
+ if (!(0, import_node_fs45.existsSync)(exitFile)) {
17963
+ try {
17964
+ (0, import_node_fs45.writeFileSync)(exitFile, "1\n");
17965
+ } catch {
17966
+ }
17967
+ }
17968
+ snap = snapshotJob(dir);
17969
+ const stillRunning = jobTreeAlive(targetPid) || snap.running;
17970
+ const result = toResult(jobId, snap, { failed: true });
17971
+ const stopped = !stillRunning;
17972
+ return {
17973
+ ...result,
17974
+ stillRunning,
17975
+ ok: false,
17976
+ failed: true,
17977
+ status: stillRunning ? "running" : "failed",
17978
+ stopped,
17979
+ reason: stopped ? "stopped" : "still-running",
17980
+ stopReason: why || void 0,
17981
+ hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
17982
+ };
17983
+ }
17984
+ 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;
17985
+ var init_wait_for_job = __esm({
17986
+ "src/mcp/wait-for-job.ts"() {
17987
+ "use strict";
17988
+ import_node_fs45 = require("fs");
17989
+ import_node_path45 = require("path");
17990
+ init_workspace_scratch();
17991
+ init_wait_for_turn();
17992
+ MAX_JOB_CONTINUES = 8;
17993
+ MAX_JOB_DELTA_LINES = 80;
17994
+ MAX_JOB_DELTA_CHARS = 4e3;
17995
+ DEFERRED_DONE_MAX_CHARS = 480;
17996
+ GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
17997
+ 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.";
17998
+ 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.";
17999
+ JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
18000
+ }
18001
+ });
18002
+
17555
18003
  // src/hook/conductor.ts
17556
18004
  function matchSimpleGlob(pattern, name) {
17557
18005
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
17558
18006
  return new RegExp(`^${escaped}$`).test(name);
17559
18007
  }
17560
18008
  function readWorktreeInclude(repoPath) {
17561
- const path2 = (0, import_node_path45.join)(repoPath, ".worktreeinclude");
17562
- if (!(0, import_node_fs45.existsSync)(path2)) return [];
17563
- return (0, import_node_fs45.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
18009
+ const path2 = (0, import_node_path46.join)(repoPath, ".worktreeinclude");
18010
+ if (!(0, import_node_fs46.existsSync)(path2)) return [];
18011
+ return (0, import_node_fs46.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
17564
18012
  }
17565
18013
  function resolveFilesToCopy(repoPath) {
17566
18014
  const fromInclude = readWorktreeInclude(repoPath);
@@ -17570,10 +18018,10 @@ function resolveFilesToCopy(repoPath) {
17570
18018
  if (settings?.fileIncludeGlobs?.length) {
17571
18019
  const matched = [];
17572
18020
  try {
17573
- for (const entry of (0, import_node_fs45.readdirSync)(repoPath, { withFileTypes: true })) {
18021
+ for (const entry of (0, import_node_fs46.readdirSync)(repoPath, { withFileTypes: true })) {
17574
18022
  if (!entry.isFile()) continue;
17575
18023
  for (const glob of settings.fileIncludeGlobs) {
17576
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path45.basename)(glob), entry.name)) {
18024
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path46.basename)(glob), entry.name)) {
17577
18025
  matched.push(entry.name);
17578
18026
  break;
17579
18027
  }
@@ -17585,7 +18033,7 @@ function resolveFilesToCopy(repoPath) {
17585
18033
  }
17586
18034
  const defaults = [];
17587
18035
  try {
17588
- for (const entry of (0, import_node_fs45.readdirSync)(repoPath, { withFileTypes: true })) {
18036
+ for (const entry of (0, import_node_fs46.readdirSync)(repoPath, { withFileTypes: true })) {
17589
18037
  if (entry.isFile() && entry.name.startsWith(".env")) {
17590
18038
  defaults.push(entry.name);
17591
18039
  }
@@ -17599,11 +18047,11 @@ function copyConfiguredFiles(repoPath, worktreePath) {
17599
18047
  const patterns = resolveFilesToCopy(repoPath);
17600
18048
  const copied = [];
17601
18049
  for (const rel of patterns) {
17602
- const src = (0, import_node_path45.join)(repoPath, rel);
17603
- if (!(0, import_node_fs45.existsSync)(src)) continue;
17604
- const dest = (0, import_node_path45.join)(worktreePath, rel);
17605
- (0, import_node_fs45.mkdirSync)((0, import_node_path45.dirname)(dest), { recursive: true });
17606
- (0, import_node_fs45.copyFileSync)(src, dest);
18050
+ const src = (0, import_node_path46.join)(repoPath, rel);
18051
+ if (!(0, import_node_fs46.existsSync)(src)) continue;
18052
+ const dest = (0, import_node_path46.join)(worktreePath, rel);
18053
+ (0, import_node_fs46.mkdirSync)((0, import_node_path46.dirname)(dest), { recursive: true });
18054
+ (0, import_node_fs46.copyFileSync)(src, dest);
17607
18055
  copied.push(rel);
17608
18056
  }
17609
18057
  return copied;
@@ -17638,7 +18086,7 @@ function buildWorkspaceScriptEnv(opts, baseEnv) {
17638
18086
  const env = stripNestedElectronEnv({
17639
18087
  ...baseEnv ?? process.env
17640
18088
  });
17641
- const name = opts.workspaceName ?? (0, import_node_path45.basename)(opts.worktreePath);
18089
+ const name = opts.workspaceName ?? (0, import_node_path46.basename)(opts.worktreePath);
17642
18090
  const ports = opts.ports ?? [];
17643
18091
  const primary = ports[0];
17644
18092
  env.SIDEBOARD_WORKSPACE_NAME = name;
@@ -17707,20 +18155,20 @@ function listenerCwd(pid) {
17707
18155
  }
17708
18156
  function realpathOrResolve(path2) {
17709
18157
  try {
17710
- return (0, import_node_fs45.realpathSync)(path2);
18158
+ return (0, import_node_fs46.realpathSync)(path2);
17711
18159
  } catch {
17712
- return (0, import_node_path45.resolve)(path2);
18160
+ return (0, import_node_path46.resolve)(path2);
17713
18161
  }
17714
18162
  }
17715
18163
  function isOtherWorktreeListenerCwd(cwd, worktreePath) {
17716
18164
  const wt = realpathOrResolve(worktreePath);
17717
18165
  const c = realpathOrResolve(cwd);
17718
- if (c === wt || c.startsWith(wt + import_node_path45.sep)) return false;
17719
- const parent = (0, import_node_path45.dirname)(wt);
17720
- const thisName = (0, import_node_path45.basename)(wt);
18166
+ if (c === wt || c.startsWith(wt + import_node_path46.sep)) return false;
18167
+ const parent = (0, import_node_path46.dirname)(wt);
18168
+ const thisName = (0, import_node_path46.basename)(wt);
17721
18169
  if (!thisName || c === parent) return false;
17722
- if (!c.startsWith(parent + import_node_path45.sep)) return false;
17723
- const first = c.slice(parent.length + 1).split(import_node_path45.sep)[0];
18170
+ if (!c.startsWith(parent + import_node_path46.sep)) return false;
18171
+ const first = c.slice(parent.length + 1).split(import_node_path46.sep)[0];
17724
18172
  return Boolean(first) && first !== thisName;
17725
18173
  }
17726
18174
  function signalPid(pid, signal) {
@@ -17732,12 +18180,16 @@ function signalPid(pid, signal) {
17732
18180
  function isThisWorktreeListenerCwd(cwd, worktreePath) {
17733
18181
  const wt = realpathOrResolve(worktreePath);
17734
18182
  const c = realpathOrResolve(cwd);
17735
- return c === wt || c.startsWith(wt + import_node_path45.sep);
18183
+ return c === wt || c.startsWith(wt + import_node_path46.sep);
17736
18184
  }
17737
18185
  function killDetachedRunTree(pid, worktreePath) {
17738
18186
  if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
17739
18187
  const cwd = listenerCwd(pid);
17740
- if (!cwd || !isThisWorktreeListenerCwd(cwd, worktreePath)) return;
18188
+ if (cwd) {
18189
+ if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
18190
+ } else if (!processGroupAlive(pid)) {
18191
+ return;
18192
+ }
17741
18193
  try {
17742
18194
  process.kill(-pid, "SIGTERM");
17743
18195
  } catch {
@@ -18134,14 +18586,14 @@ async function startDevServer(repoPath, worktreePath, onLine, opts) {
18134
18586
  throw err;
18135
18587
  }
18136
18588
  }
18137
- var import_node_fs45, import_node_child_process5, import_node_net, import_node_path45, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
18589
+ var import_node_fs46, import_node_child_process5, import_node_net, import_node_path46, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
18138
18590
  var init_conductor = __esm({
18139
18591
  "src/hook/conductor.ts"() {
18140
18592
  "use strict";
18141
- import_node_fs45 = require("fs");
18593
+ import_node_fs46 = require("fs");
18142
18594
  import_node_child_process5 = require("child_process");
18143
18595
  import_node_net = require("net");
18144
- import_node_path45 = require("path");
18596
+ import_node_path46 = require("path");
18145
18597
  import_execa4 = require("execa");
18146
18598
  import_node_readline3 = require("readline");
18147
18599
  init_settings();
@@ -18153,6 +18605,7 @@ var init_conductor = __esm({
18153
18605
  init_request_review();
18154
18606
  init_worktree_exclude();
18155
18607
  init_worktree_run_ports();
18608
+ init_wait_for_job();
18156
18609
  init_worktree_run_ports();
18157
18610
  init_nested_electron_env();
18158
18611
  PORT_RANGE_SIZE = 10;
@@ -18173,9 +18626,9 @@ async function findOrphanWorktrees(repoPaths) {
18173
18626
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
18174
18627
  );
18175
18628
  const homeRoot = sideboardWorkspacesDir();
18176
- if ((0, import_node_fs46.existsSync)(homeRoot)) {
18629
+ if ((0, import_node_fs47.existsSync)(homeRoot)) {
18177
18630
  try {
18178
- for (const entry of (0, import_node_fs46.readdirSync)(homeRoot, { withFileTypes: true })) {
18631
+ for (const entry of (0, import_node_fs47.readdirSync)(homeRoot, { withFileTypes: true })) {
18179
18632
  if (!entry.isDirectory()) continue;
18180
18633
  void entry;
18181
18634
  }
@@ -18185,7 +18638,7 @@ async function findOrphanWorktrees(repoPaths) {
18185
18638
  const orphans = [];
18186
18639
  const seen = /* @__PURE__ */ new Set();
18187
18640
  for (const repoPath of repos) {
18188
- if (!repoPath || !(0, import_node_fs46.existsSync)(repoPath)) continue;
18641
+ if (!repoPath || !(0, import_node_fs47.existsSync)(repoPath)) continue;
18189
18642
  try {
18190
18643
  const wts = await listWorktrees(repoPath);
18191
18644
  for (const wt of wts) {
@@ -18196,7 +18649,7 @@ async function findOrphanWorktrees(repoPaths) {
18196
18649
  seen.add(path2);
18197
18650
  let mtimeMs = 0;
18198
18651
  try {
18199
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18652
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18200
18653
  } catch {
18201
18654
  mtimeMs = 0;
18202
18655
  }
@@ -18206,16 +18659,16 @@ async function findOrphanWorktrees(repoPaths) {
18206
18659
  }
18207
18660
  try {
18208
18661
  const root = worktreesRoot(repoPath);
18209
- if ((0, import_node_fs46.existsSync)(root)) {
18210
- for (const entry of (0, import_node_fs46.readdirSync)(root, { withFileTypes: true })) {
18662
+ if ((0, import_node_fs47.existsSync)(root)) {
18663
+ for (const entry of (0, import_node_fs47.readdirSync)(root, { withFileTypes: true })) {
18211
18664
  if (!entry.isDirectory()) continue;
18212
- const path2 = (0, import_node_path46.join)(root, entry.name).replace(/\/$/, "");
18665
+ const path2 = (0, import_node_path47.join)(root, entry.name).replace(/\/$/, "");
18213
18666
  if (known.has(path2) || seen.has(path2)) continue;
18214
- if (!(0, import_node_fs46.existsSync)((0, import_node_path46.join)(path2, ".git"))) continue;
18667
+ if (!(0, import_node_fs47.existsSync)((0, import_node_path47.join)(path2, ".git"))) continue;
18215
18668
  seen.add(path2);
18216
18669
  let mtimeMs = 0;
18217
18670
  try {
18218
- mtimeMs = (0, import_node_fs46.statSync)(path2).mtimeMs;
18671
+ mtimeMs = (0, import_node_fs47.statSync)(path2).mtimeMs;
18219
18672
  } catch {
18220
18673
  mtimeMs = Date.now();
18221
18674
  }
@@ -18276,12 +18729,12 @@ function worktreeCleanupSettings() {
18276
18729
  autoCleanupOrphans: a.autoCleanupOrphans
18277
18730
  };
18278
18731
  }
18279
- var import_node_fs46, import_node_path46;
18732
+ var import_node_fs47, import_node_path47;
18280
18733
  var init_orphan_cleanup = __esm({
18281
18734
  "src/git/orphan-cleanup.ts"() {
18282
18735
  "use strict";
18283
- import_node_fs46 = require("fs");
18284
- import_node_path46 = require("path");
18736
+ import_node_fs47 = require("fs");
18737
+ import_node_path47 = require("path");
18285
18738
  init_worktree();
18286
18739
  init_thread_store();
18287
18740
  init_paths();
@@ -18297,6 +18750,7 @@ function stripArchivedTranscript(thread) {
18297
18750
  attachments: [],
18298
18751
  pendingTurnAttachments: [],
18299
18752
  queueAttachments: [],
18753
+ compactedMessages: void 0,
18300
18754
  messages: [historyRetentionStubMessage(thread)]
18301
18755
  },
18302
18756
  { touch: false }
@@ -18456,12 +18910,12 @@ async function cloneRepoIntoSideboard(opts) {
18456
18910
  if (!url) throw new Error("Clone URL is required");
18457
18911
  let name = opts.name?.trim();
18458
18912
  if (!name) {
18459
- const leaf = (0, import_node_path47.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18913
+ const leaf = (0, import_node_path48.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
18460
18914
  name = leaf || "repo";
18461
18915
  }
18462
18916
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
18463
- const dest = (0, import_node_path47.join)(sideboardReposDir(), name);
18464
- if ((0, import_node_fs47.existsSync)(dest)) {
18917
+ const dest = (0, import_node_path48.join)(sideboardReposDir(), name);
18918
+ if ((0, import_node_fs48.existsSync)(dest)) {
18465
18919
  const repoPath2 = await resolveRepoRoot(dest);
18466
18920
  const workspace2 = await ensureWorkspace(repoPath2);
18467
18921
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -18476,12 +18930,12 @@ async function cloneRepoIntoSideboard(opts) {
18476
18930
  const workspace = await ensureWorkspace(repoPath);
18477
18931
  return { repoPath, workspace };
18478
18932
  }
18479
- var import_node_fs47, import_node_path47, import_execa6;
18933
+ var import_node_fs48, import_node_path48, import_execa6;
18480
18934
  var init_clone_repo = __esm({
18481
18935
  "src/git/clone-repo.ts"() {
18482
18936
  "use strict";
18483
- import_node_fs47 = require("fs");
18484
- import_node_path47 = require("path");
18937
+ import_node_fs48 = require("fs");
18938
+ import_node_path48 = require("path");
18485
18939
  import_execa6 = require("execa");
18486
18940
  init_paths();
18487
18941
  init_workspaces();
@@ -18539,394 +18993,6 @@ var init_child_halt = __esm({
18539
18993
  }
18540
18994
  });
18541
18995
 
18542
- // src/mcp/wait-for-turn.ts
18543
- function mcpWaitForTurnTimeoutMs(requested) {
18544
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
18545
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
18546
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
18547
- }
18548
- function mcpWaitTaskHint(taskState, status) {
18549
- switch (taskState) {
18550
- case "submitted":
18551
- return MCP_WAIT_QUEUED_HINT;
18552
- case "working":
18553
- return MCP_WAIT_STILL_RUNNING_HINT;
18554
- case "input-required":
18555
- return MCP_WAIT_INPUT_REQUIRED_HINT;
18556
- case "canceled":
18557
- return MCP_WAIT_STOPPED_HINT;
18558
- case "failed":
18559
- return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
18560
- default:
18561
- return void 0;
18562
- }
18563
- }
18564
- var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT, MCP_WAIT_INPUT_REQUIRED_HINT;
18565
- var init_wait_for_turn = __esm({
18566
- "src/mcp/wait-for-turn.ts"() {
18567
- "use strict";
18568
- MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
18569
- MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
18570
- MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
18571
- MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
18572
- MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
18573
- MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
18574
- MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
18575
- }
18576
- });
18577
-
18578
- // src/mcp/wait-for-job.ts
18579
- function mcpWaitForJobTimeoutMs(requested) {
18580
- return mcpWaitForTurnTimeoutMs(requested);
18581
- }
18582
- function sanitizeDetachedJobId(id) {
18583
- const s = id.trim();
18584
- if (!JOB_ID_RE.test(s)) {
18585
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
18586
- }
18587
- return s;
18588
- }
18589
- function jobAlive(pid) {
18590
- if (!Number.isInteger(pid) || pid <= 0) return false;
18591
- try {
18592
- process.kill(pid, 0);
18593
- return true;
18594
- } catch {
18595
- return false;
18596
- }
18597
- }
18598
- function processGroupAlive(pgid) {
18599
- if (process.platform === "win32") return false;
18600
- if (!Number.isInteger(pgid) || pgid <= 1) return false;
18601
- try {
18602
- process.kill(-pgid, 0);
18603
- return true;
18604
- } catch (err) {
18605
- return err && typeof err === "object" && "code" in err && err.code === "EPERM";
18606
- }
18607
- }
18608
- function jobTreeAlive(pid) {
18609
- if (pid == null) return false;
18610
- return jobAlive(pid) || processGroupAlive(pid);
18611
- }
18612
- function readIntFile(file) {
18613
- if (!(0, import_node_fs48.existsSync)(file)) return null;
18614
- const n = Number.parseInt((0, import_node_fs48.readFileSync)(file, "utf8").trim(), 10);
18615
- return Number.isInteger(n) ? n : null;
18616
- }
18617
- function jobDir(root, id, legacy = false) {
18618
- return (0, import_node_path48.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
18619
- }
18620
- function resolveJobDir(root, id) {
18621
- const modern = jobDir(root, id, false);
18622
- if ((0, import_node_fs48.existsSync)(modern)) return modern;
18623
- const legacy = jobDir(root, id, true);
18624
- if ((0, import_node_fs48.existsSync)(legacy)) return legacy;
18625
- return modern;
18626
- }
18627
- function listJobIdsIn(root, rel) {
18628
- const dir = (0, import_node_path48.join)(root, rel);
18629
- if (!(0, import_node_fs48.existsSync)(dir)) return [];
18630
- return (0, import_node_fs48.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
18631
- }
18632
- function listRunningDetachedJobs(worktreePath) {
18633
- const root = worktreePath.trim();
18634
- if (!root) return [];
18635
- const ids = /* @__PURE__ */ new Set([
18636
- ...listJobIdsIn(root, DETACHED_JOBS_DIR),
18637
- ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
18638
- ]);
18639
- const running = [];
18640
- for (const id of ids) {
18641
- const dir = resolveJobDir(root, id);
18642
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18643
- if (jobTreeAlive(pid)) running.push(id);
18644
- }
18645
- return running.sort();
18646
- }
18647
- function looksLikeDeferredDonePromise(text7) {
18648
- const t = (text7 ?? "").trim();
18649
- if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
18650
- const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
18651
- if (!unquoted) return false;
18652
- return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
18653
- }
18654
- function isJobContinuePrompt(text7) {
18655
- if (typeof text7 !== "string") return false;
18656
- const t = text7.trim();
18657
- return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
18658
- }
18659
- function collapseGhRunWatchLog(text7) {
18660
- if (!text7.includes(GH_WATCH_REFRESH)) return text7;
18661
- const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
18662
- const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
18663
- if (watchFrames.length <= 1) return text7;
18664
- const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
18665
- const last = watchFrames[watchFrames.length - 1] ?? "";
18666
- const omitted = watchFrames.length - 1;
18667
- const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
18668
- `;
18669
- return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
18670
- }
18671
- function capJobLogDelta(text7) {
18672
- const collapsed = collapseGhRunWatchLog(text7);
18673
- const lines = collapsed.split("\n");
18674
- let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
18675
- ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
18676
- if (out.length > MAX_JOB_DELTA_CHARS) {
18677
- out = `\u2026(truncated ${out.length} chars)
18678
- ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
18679
- }
18680
- return out;
18681
- }
18682
- function formatJobStillRunningContinuePrompt(jobIds) {
18683
- const ids = jobIds.join(", ");
18684
- return [
18685
- `Detached job still running: ${ids}.`,
18686
- "Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
18687
- "If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
18688
- "Then report the result. Do not tell the user you will let them know later."
18689
- ].join(" ");
18690
- }
18691
- function formatDeferredDoneContinuePrompt() {
18692
- return [
18693
- "You ended the turn after promising to report later, but no detached job is running.",
18694
- "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
18695
- "Do not say you will let the user know later."
18696
- ].join(" ");
18697
- }
18698
- function turnWatchedDetachedJob(parts) {
18699
- return parts.some((p) => {
18700
- if (p.type !== "tool") return false;
18701
- if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
18702
- const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
18703
- return /detached-job\.(?:js|cjs)\b/i.test(blob);
18704
- });
18705
- }
18706
- function planJobContinue(opts) {
18707
- if (opts.isOrchestrator) return { action: "none" };
18708
- if (opts.agent === "brightsy") return { action: "none" };
18709
- if (opts.queueLength > 0) return { action: "none" };
18710
- if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
18711
- const farewell = looksLikeDeferredDonePromise(opts.chatText);
18712
- if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
18713
- return {
18714
- action: "wait",
18715
- jobIds: opts.runningJobIds,
18716
- prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
18717
- };
18718
- }
18719
- if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
18720
- return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
18721
- }
18722
- return { action: "none" };
18723
- }
18724
- function tailProgress(logFile, maxLines = 12) {
18725
- if (!(0, import_node_fs48.existsSync)(logFile)) return "(no log yet)";
18726
- const lines = (0, import_node_fs48.readFileSync)(logFile, "utf8").split("\n");
18727
- return lines.slice(-maxLines).join("\n");
18728
- }
18729
- function lastProgressLine(progress) {
18730
- const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
18731
- return last ? last.slice(0, 72) : void 0;
18732
- }
18733
- function readLogLines(file) {
18734
- if (!(0, import_node_fs48.existsSync)(file)) return [];
18735
- const lines = (0, import_node_fs48.readFileSync)(file, "utf8").split("\n");
18736
- if (lines.length && lines[lines.length - 1] === "") lines.pop();
18737
- return lines;
18738
- }
18739
- function takeDelta(logFile, cursorFile) {
18740
- const lines = readLogLines(logFile);
18741
- const cursor = readIntFile(cursorFile) ?? 0;
18742
- const start = Math.min(Math.max(0, cursor), lines.length);
18743
- return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
18744
- }
18745
- function snapshotJob(dir) {
18746
- const pid = readIntFile((0, import_node_path48.join)(dir, "pid"));
18747
- const running = jobTreeAlive(pid);
18748
- return {
18749
- pid,
18750
- running,
18751
- exitCode: readIntFile((0, import_node_path48.join)(dir, "exit")),
18752
- log: (0, import_node_path48.join)(dir, "log"),
18753
- cursor: (0, import_node_path48.join)(dir, "present.cursor"),
18754
- progress: tailProgress((0, import_node_path48.join)(dir, "log"))
18755
- };
18756
- }
18757
- function toResult(id, snap, extra) {
18758
- const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
18759
- const ok = !snap.running && snap.exitCode === 0;
18760
- const stillRunning = snap.running && !ok;
18761
- const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
18762
- try {
18763
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(snap.cursor), { recursive: true });
18764
- (0, import_node_fs48.writeFileSync)(snap.cursor, `${nextCursor}
18765
- `);
18766
- } catch {
18767
- }
18768
- const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
18769
- const progress = extra?.progress ?? snap.progress;
18770
- return {
18771
- stillRunning,
18772
- ok,
18773
- failed: Boolean(failed && !stillRunning && !ok),
18774
- status,
18775
- id,
18776
- pid: snap.pid,
18777
- exitCode: snap.exitCode ?? void 0,
18778
- phase: lastProgressLine(progress),
18779
- delta,
18780
- progress,
18781
- hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
18782
- };
18783
- }
18784
- async function sleepMs(ms) {
18785
- await new Promise((resolve2) => setTimeout(resolve2, ms));
18786
- }
18787
- async function waitForDetachedJob(cwd, id, opts) {
18788
- const jobId = sanitizeDetachedJobId(id);
18789
- const root = cwd.trim() || process.cwd();
18790
- const dir = resolveJobDir(root, jobId);
18791
- const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
18792
- const sleep2 = opts?.sleep ?? sleepMs;
18793
- if (!(0, import_node_fs48.existsSync)(dir)) {
18794
- return {
18795
- stillRunning: false,
18796
- ok: false,
18797
- failed: true,
18798
- status: "failed",
18799
- id: jobId,
18800
- delta: "",
18801
- progress: "No detached job. Start one first.",
18802
- hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
18803
- };
18804
- }
18805
- const deadline = Date.now() + timeoutMs;
18806
- let snap = snapshotJob(dir);
18807
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18808
- if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
18809
- return toResult(jobId, snap, {
18810
- failed: true,
18811
- progress: "No detached job. Start one first."
18812
- });
18813
- }
18814
- while (Date.now() < deadline) {
18815
- snap = snapshotJob(dir);
18816
- if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
18817
- if (!snap.running) return toResult(jobId, snap, { failed: true });
18818
- await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
18819
- }
18820
- snap = snapshotJob(dir);
18821
- return toResult(jobId, snap);
18822
- }
18823
- function killProcessTree(pid, signal) {
18824
- if (!Number.isInteger(pid) || pid <= 0) return;
18825
- if (process.platform !== "win32") {
18826
- try {
18827
- process.kill(-pid, signal);
18828
- return;
18829
- } catch {
18830
- }
18831
- }
18832
- try {
18833
- process.kill(pid, signal);
18834
- } catch {
18835
- }
18836
- }
18837
- function appendJobLog(logFile, line) {
18838
- try {
18839
- (0, import_node_fs48.mkdirSync)((0, import_node_path48.dirname)(logFile), { recursive: true });
18840
- const body = line.endsWith("\n") ? line : `${line}
18841
- `;
18842
- (0, import_node_fs48.appendFileSync)(logFile, body);
18843
- } catch {
18844
- }
18845
- }
18846
- async function stopDetachedJob(cwd, id, opts) {
18847
- const jobId = sanitizeDetachedJobId(id);
18848
- const root = cwd.trim() || process.cwd();
18849
- const dir = resolveJobDir(root, jobId);
18850
- const sleep2 = opts?.sleep ?? sleepMs;
18851
- const why = opts?.reason?.trim() ?? "";
18852
- if (!(0, import_node_fs48.existsSync)(dir)) {
18853
- return {
18854
- stillRunning: false,
18855
- ok: false,
18856
- failed: true,
18857
- status: "failed",
18858
- id: jobId,
18859
- delta: "",
18860
- progress: "No detached job. Start one first.",
18861
- hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
18862
- stopped: false,
18863
- reason: "not-found"
18864
- };
18865
- }
18866
- let snap = snapshotJob(dir);
18867
- if (!snap.running) {
18868
- const result2 = toResult(jobId, snap);
18869
- return {
18870
- ...result2,
18871
- stopped: false,
18872
- reason: "not-running",
18873
- hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
18874
- };
18875
- }
18876
- appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
18877
- const targetPid = snap.pid;
18878
- if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
18879
- const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
18880
- const deadline = Date.now() + graceMs;
18881
- while (Date.now() < deadline && jobTreeAlive(targetPid)) {
18882
- await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
18883
- }
18884
- if (targetPid != null) {
18885
- killProcessTree(targetPid, "SIGKILL");
18886
- await sleep2(100);
18887
- }
18888
- const exitFile = (0, import_node_path48.join)(dir, "exit");
18889
- if (!(0, import_node_fs48.existsSync)(exitFile)) {
18890
- try {
18891
- (0, import_node_fs48.writeFileSync)(exitFile, "1\n");
18892
- } catch {
18893
- }
18894
- }
18895
- snap = snapshotJob(dir);
18896
- const stillRunning = jobTreeAlive(targetPid) || snap.running;
18897
- const result = toResult(jobId, snap, { failed: true });
18898
- const stopped = !stillRunning;
18899
- return {
18900
- ...result,
18901
- stillRunning,
18902
- ok: false,
18903
- failed: true,
18904
- status: stillRunning ? "running" : "failed",
18905
- stopped,
18906
- reason: stopped ? "stopped" : "still-running",
18907
- stopReason: why || void 0,
18908
- hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
18909
- };
18910
- }
18911
- var import_node_fs48, import_node_path48, MAX_JOB_CONTINUES, MAX_JOB_DELTA_LINES, MAX_JOB_DELTA_CHARS, DEFERRED_DONE_MAX_CHARS, GH_WATCH_REFRESH, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
18912
- var init_wait_for_job = __esm({
18913
- "src/mcp/wait-for-job.ts"() {
18914
- "use strict";
18915
- import_node_fs48 = require("fs");
18916
- import_node_path48 = require("path");
18917
- init_workspace_scratch();
18918
- init_wait_for_turn();
18919
- MAX_JOB_CONTINUES = 8;
18920
- MAX_JOB_DELTA_LINES = 80;
18921
- MAX_JOB_DELTA_CHARS = 4e3;
18922
- DEFERRED_DONE_MAX_CHARS = 480;
18923
- GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
18924
- MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
18925
- MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
18926
- JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
18927
- }
18928
- });
18929
-
18930
18996
  // src/detect/detect.ts
18931
18997
  async function detectAgents() {
18932
18998
  ensureAgentPath();
@@ -23230,7 +23296,7 @@ function formatWorktreeDirective(thread, opts) {
23230
23296
  const slug = opts?.githubSlug ?? "<origin-owner/name>";
23231
23297
  const resolve2 = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
23232
23298
  lines.push(
23233
- `- 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\`.`
23299
+ `- 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\`.`
23234
23300
  );
23235
23301
  }
23236
23302
  lines.push(
@@ -23709,6 +23775,13 @@ function isPresentPlanToolName(name) {
23709
23775
  if (!name) return false;
23710
23776
  return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
23711
23777
  }
23778
+ function threadMayPresentPlan(thread) {
23779
+ return Boolean(thread?.planMode);
23780
+ }
23781
+ function shouldRegisterPresentPlan(thread) {
23782
+ if (thread == null) return true;
23783
+ return threadMayPresentPlan(thread);
23784
+ }
23712
23785
  function extractPresentedPlan(parts) {
23713
23786
  if (!parts?.length) return null;
23714
23787
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -23746,7 +23819,7 @@ function resolvePlanMarkdown(opts) {
23746
23819
  }
23747
23820
  return null;
23748
23821
  }
23749
- var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
23822
+ var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
23750
23823
  var init_plan_present = __esm({
23751
23824
  "src/plan/plan-present.ts"() {
23752
23825
  "use strict";
@@ -23754,6 +23827,8 @@ var init_plan_present = __esm({
23754
23827
  PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
23755
23828
  PLAN_FILE_NAME = "plan.md";
23756
23829
  LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
23830
+ 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.";
23831
+ PRESENT_PLAN_REQUIRES_PLAN_MODE = "Plan mode is off. Do not call present_plan or ExitPlanMode. Write the plan in chat. The user can turn Plan on in the composer.";
23757
23832
  }
23758
23833
  });
23759
23834
 
@@ -23763,11 +23838,15 @@ __export(plan_file_exports, {
23763
23838
  LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
23764
23839
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
23765
23840
  PLAN_FILE_REL: () => PLAN_FILE_REL,
23841
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
23842
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
23766
23843
  extractPresentedPlan: () => extractPresentedPlan,
23767
23844
  isPresentPlanToolName: () => isPresentPlanToolName,
23768
23845
  planFileAbs: () => planFileAbs,
23769
23846
  readPlanFile: () => readPlanFile,
23770
23847
  resolvePlanMarkdown: () => resolvePlanMarkdown,
23848
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
23849
+ threadMayPresentPlan: () => threadMayPresentPlan,
23771
23850
  writePlanFile: () => writePlanFile
23772
23851
  });
23773
23852
  function ensureAttachmentsGitignore(worktreePath) {
@@ -24903,7 +24982,8 @@ var init_orchestrator = __esm({
24903
24982
  if (compact.didCompact) {
24904
24983
  thread = updateThread(threadId, {
24905
24984
  messages: compact.thread.messages,
24906
- sessionId: compact.thread.sessionId
24985
+ sessionId: compact.thread.sessionId,
24986
+ compactedMessages: compact.thread.compactedMessages
24907
24987
  });
24908
24988
  this.emit({
24909
24989
  type: "context_compacted",
@@ -24917,9 +24997,6 @@ var init_orchestrator = __esm({
24917
24997
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
24918
24998
  thread = this.requireThread(threadId);
24919
24999
  }
24920
- if (this.stoppedTurns.has(threadId)) {
24921
- return;
24922
- }
24923
25000
  const promptText = typeof prompt === "string" ? prompt : "";
24924
25001
  const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
24925
25002
  const parked = takePendingTurnAttachments(thread);
@@ -24936,6 +25013,14 @@ var init_orchestrator = __esm({
24936
25013
  agent: thread.agent,
24937
25014
  orch: isOrchestratorThread(thread)
24938
25015
  });
25016
+ if (this.stoppedTurns.has(threadId)) {
25017
+ const stopped = writeLiveStatus(threadId, "stopped");
25018
+ if (stopped?.status === "stopped") {
25019
+ this.emit({ type: "status_changed", threadId, status: "stopped" });
25020
+ }
25021
+ this.emit({ type: "turn_finished", threadId, exitCode: 1 });
25022
+ return;
25023
+ }
24939
25024
  thread = this.requireThread(threadId);
24940
25025
  const gitPrompt = expandCanonicalGitRequest(promptText);
24941
25026
  const { agentPrompt: expandedPrompt } = expandComposerPrompt(
@@ -24994,7 +25079,7 @@ var init_orchestrator = __esm({
24994
25079
  ].filter(Boolean).join("\n\n") || void 0;
24995
25080
  const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
24996
25081
  const agentPrompt = [
24997
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
25082
+ planModeTurnInstruction(thread.planMode),
24998
25083
  orchestrationReminder,
24999
25084
  prGateDirective,
25000
25085
  injectedNoticeContext,
@@ -26928,8 +27013,11 @@ __export(index_exports, {
26928
27013
  PLAN_FILE_NAME: () => PLAN_FILE_NAME,
26929
27014
  PLAN_FILE_REL: () => PLAN_FILE_REL,
26930
27015
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
27016
+ PLAN_MODE_OFF_INSTRUCTION: () => PLAN_MODE_OFF_INSTRUCTION,
26931
27017
  PLAN_QUESTION_ANSWERS_PREFIX: () => PLAN_QUESTION_ANSWERS_PREFIX,
26932
27018
  PORT_RANGE_SIZE: () => PORT_RANGE_SIZE,
27019
+ PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
27020
+ PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
26933
27021
  PROFILE_NOTES_MAX: () => PROFILE_NOTES_MAX,
26934
27022
  PR_DETAILS_JSON_FIELDS: () => PR_DETAILS_JSON_FIELDS,
26935
27023
  REPO_REVIEW_NAME: () => REPO_REVIEW_NAME,
@@ -27000,6 +27088,7 @@ __export(index_exports, {
27000
27088
  annotateDevAccessRuns: () => annotateDevAccessRuns,
27001
27089
  appDataDir: () => appDataDir,
27002
27090
  appSettingsPath: () => appSettingsPath,
27091
+ appendCompactedMessages: () => appendCompactedMessages,
27003
27092
  appendIndexedGitConfig: () => appendIndexedGitConfig,
27004
27093
  appendMessage: () => appendMessage,
27005
27094
  appendQueuedItem: () => appendQueuedItem,
@@ -27087,6 +27176,9 @@ __export(index_exports, {
27087
27176
  commentGitHubIssue: () => commentGitHubIssue,
27088
27177
  commentLinearIssue: () => commentLinearIssue,
27089
27178
  commitAll: () => commitAll,
27179
+ compactArchiveProse: () => compactArchiveProse,
27180
+ compactPartsForSummaryMessage: () => compactPartsForSummaryMessage,
27181
+ compactSummaryPart: () => compactSummaryPart,
27090
27182
  computeNextRunAt: () => computeNextRunAt,
27091
27183
  conductorBundledBinDir: () => conductorBundledBinDir,
27092
27184
  conductorDbPath: () => conductorDbPath,
@@ -27294,6 +27386,7 @@ __export(index_exports, {
27294
27386
  getWorktreeDirtyStat: () => getWorktreeDirtyStat,
27295
27387
  gh: () => gh,
27296
27388
  ghHeadRef: () => ghHeadRef,
27389
+ ghPrCreateAssigneeArgs: () => ghPrCreateAssigneeArgs,
27297
27390
  ghRepoSelectArgs: () => ghRepoSelectArgs,
27298
27391
  git: () => git,
27299
27392
  gitBranchPrefixSetting: () => gitBranchPrefixSetting,
@@ -27486,6 +27579,7 @@ __export(index_exports, {
27486
27579
  mergeSideboardIntoMcpServersJson: () => mergeSideboardIntoMcpServersJson,
27487
27580
  mergeUsage: () => mergeUsage,
27488
27581
  messagePartParentId: () => messagePartParentId,
27582
+ messagesForCompactArchive: () => messagesForCompactArchive,
27489
27583
  messagesSinceLastBrightsyContextSummary: () => messagesSinceLastBrightsyContextSummary,
27490
27584
  moveQueuedItemToFront: () => moveQueuedItemToFront,
27491
27585
  needsCoordinatorAction: () => needsCoordinatorAction,
@@ -27545,6 +27639,7 @@ __export(index_exports, {
27545
27639
  planFileAbs: () => planFileAbs,
27546
27640
  planHistoryAgePurge: () => planHistoryAgePurge,
27547
27641
  planHistoryRetention: () => planHistoryRetention,
27642
+ planModeTurnInstruction: () => planModeTurnInstruction,
27548
27643
  planQuestionsSignature: () => planQuestionsSignature,
27549
27644
  pnpmFilterPackage: () => pnpmFilterPackage,
27550
27645
  pollSlackOutboundWatches: () => pollSlackOutboundWatches,
@@ -27672,6 +27767,7 @@ __export(index_exports, {
27672
27767
  shouldInjectBrightsyMcp: () => shouldInjectBrightsyMcp,
27673
27768
  shouldNotifyOrchestrator: () => shouldNotifyOrchestrator,
27674
27769
  shouldRefreshReviewRequestTemplate: () => shouldRefreshReviewRequestTemplate,
27770
+ shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
27675
27771
  shouldRemoveWorktreeOnTeardown: () => shouldRemoveWorktreeOnTeardown,
27676
27772
  shouldResetSessionForOccupancy: () => shouldResetSessionForOccupancy,
27677
27773
  shouldRunHistoryCleanup: () => shouldRunHistoryCleanup,
@@ -27733,6 +27829,7 @@ __export(index_exports, {
27733
27829
  threadHasCompactedContext: () => threadHasCompactedContext,
27734
27830
  threadLivePath: () => threadLivePath,
27735
27831
  threadLockPath: () => threadLockPath,
27832
+ threadMayPresentPlan: () => threadMayPresentPlan,
27736
27833
  threadRequestsBrightsyMcp: () => threadRequestsBrightsyMcp,
27737
27834
  threadsDir: () => threadsDir,
27738
27835
  threadsSharingWorktree: () => threadsSharingWorktree,
@@ -27906,7 +28003,8 @@ function slimThreadForUiList(thread) {
27906
28003
  attachments: [],
27907
28004
  pendingTurnAttachments: [],
27908
28005
  queueAttachments: [],
27909
- messages: slimMessagesForUiList(messages, thread.updatedAt)
28006
+ messages: slimMessagesForUiList(messages, thread.updatedAt),
28007
+ compactedMessages: void 0
27910
28008
  };
27911
28009
  }
27912
28010
  function slimMessagesForUiList(messages, fallbackTs) {
@@ -30460,6 +30558,7 @@ function mcpArchiveBlockedReason(thread) {
30460
30558
 
30461
30559
  // src/mcp/server.ts
30462
30560
  init_profile();
30561
+ init_plan_present();
30463
30562
 
30464
30563
  // src/mcp/run-script-ref.ts
30465
30564
  init_chat_tabs();
@@ -33372,31 +33471,40 @@ async function startMcpServer() {
33372
33471
  return mcpJson(payload);
33373
33472
  }
33374
33473
  );
33375
- server.tool(
33376
- "present_plan",
33377
- "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
33378
- {
33379
- title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
33380
- content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
33381
- thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
33382
- },
33383
- async ({ title, content, thread_id }) => {
33384
- const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
33385
- let root = process.cwd();
33386
- if (thread_id?.trim()) {
33387
- const t = orch.getThread(thread_id.trim());
33474
+ const callingThreadId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
33475
+ const callingThread = callingThreadId ? orch.getThread(callingThreadId) : null;
33476
+ if (shouldRegisterPresentPlan(callingThread)) {
33477
+ server.tool(
33478
+ "present_plan",
33479
+ PRESENT_PLAN_TOOL_DESCRIPTION,
33480
+ {
33481
+ title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
33482
+ content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
33483
+ thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
33484
+ },
33485
+ async ({ title, content, thread_id }) => {
33486
+ const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
33487
+ const ref = thread_id?.trim() || callingThreadId;
33488
+ const t = ref ? orch.getThread(ref) : null;
33489
+ if (t && !threadMayPresentPlan(t)) {
33490
+ return mcpJson(
33491
+ { ok: false, error: PRESENT_PLAN_REQUIRES_PLAN_MODE },
33492
+ true
33493
+ );
33494
+ }
33495
+ let root = process.cwd();
33388
33496
  if (t?.worktreePath?.trim()) root = t.worktreePath;
33497
+ const path2 = writePlanFile2(root, content);
33498
+ const payload = {
33499
+ ok: true,
33500
+ path: path2,
33501
+ title: title?.trim() || "Plan",
33502
+ message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
33503
+ };
33504
+ return mcpJson(payload);
33389
33505
  }
33390
- const path2 = writePlanFile2(root, content);
33391
- const payload = {
33392
- ok: true,
33393
- path: path2,
33394
- title: title?.trim() || "Plan",
33395
- message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
33396
- };
33397
- return mcpJson(payload);
33398
- }
33399
- );
33506
+ );
33507
+ }
33400
33508
  server.tool(
33401
33509
  "present_schema",
33402
33510
  "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.",
@@ -34004,7 +34112,7 @@ async function startMcpServer() {
34004
34112
  error: true,
34005
34113
  action,
34006
34114
  lastError: message,
34007
- 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
34115
+ 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
34008
34116
  },
34009
34117
  true
34010
34118
  );
@@ -36733,8 +36841,11 @@ async function startSlackRelayServer(opts) {
36733
36841
  PLAN_FILE_NAME,
36734
36842
  PLAN_FILE_REL,
36735
36843
  PLAN_MODE_INSTRUCTION,
36844
+ PLAN_MODE_OFF_INSTRUCTION,
36736
36845
  PLAN_QUESTION_ANSWERS_PREFIX,
36737
36846
  PORT_RANGE_SIZE,
36847
+ PRESENT_PLAN_REQUIRES_PLAN_MODE,
36848
+ PRESENT_PLAN_TOOL_DESCRIPTION,
36738
36849
  PROFILE_NOTES_MAX,
36739
36850
  PR_DETAILS_JSON_FIELDS,
36740
36851
  REPO_REVIEW_NAME,
@@ -36805,6 +36916,7 @@ async function startSlackRelayServer(opts) {
36805
36916
  annotateDevAccessRuns,
36806
36917
  appDataDir,
36807
36918
  appSettingsPath,
36919
+ appendCompactedMessages,
36808
36920
  appendIndexedGitConfig,
36809
36921
  appendMessage,
36810
36922
  appendQueuedItem,
@@ -36892,6 +37004,9 @@ async function startSlackRelayServer(opts) {
36892
37004
  commentGitHubIssue,
36893
37005
  commentLinearIssue,
36894
37006
  commitAll,
37007
+ compactArchiveProse,
37008
+ compactPartsForSummaryMessage,
37009
+ compactSummaryPart,
36895
37010
  computeNextRunAt,
36896
37011
  conductorBundledBinDir,
36897
37012
  conductorDbPath,
@@ -37099,6 +37214,7 @@ async function startSlackRelayServer(opts) {
37099
37214
  getWorktreeDirtyStat,
37100
37215
  gh,
37101
37216
  ghHeadRef,
37217
+ ghPrCreateAssigneeArgs,
37102
37218
  ghRepoSelectArgs,
37103
37219
  git,
37104
37220
  gitBranchPrefixSetting,
@@ -37291,6 +37407,7 @@ async function startSlackRelayServer(opts) {
37291
37407
  mergeSideboardIntoMcpServersJson,
37292
37408
  mergeUsage,
37293
37409
  messagePartParentId,
37410
+ messagesForCompactArchive,
37294
37411
  messagesSinceLastBrightsyContextSummary,
37295
37412
  moveQueuedItemToFront,
37296
37413
  needsCoordinatorAction,
@@ -37350,6 +37467,7 @@ async function startSlackRelayServer(opts) {
37350
37467
  planFileAbs,
37351
37468
  planHistoryAgePurge,
37352
37469
  planHistoryRetention,
37470
+ planModeTurnInstruction,
37353
37471
  planQuestionsSignature,
37354
37472
  pnpmFilterPackage,
37355
37473
  pollSlackOutboundWatches,
@@ -37477,6 +37595,7 @@ async function startSlackRelayServer(opts) {
37477
37595
  shouldInjectBrightsyMcp,
37478
37596
  shouldNotifyOrchestrator,
37479
37597
  shouldRefreshReviewRequestTemplate,
37598
+ shouldRegisterPresentPlan,
37480
37599
  shouldRemoveWorktreeOnTeardown,
37481
37600
  shouldResetSessionForOccupancy,
37482
37601
  shouldRunHistoryCleanup,
@@ -37538,6 +37657,7 @@ async function startSlackRelayServer(opts) {
37538
37657
  threadHasCompactedContext,
37539
37658
  threadLivePath,
37540
37659
  threadLockPath,
37660
+ threadMayPresentPlan,
37541
37661
  threadRequestsBrightsyMcp,
37542
37662
  threadsDir,
37543
37663
  threadsSharingWorktree,