@sideboard-ai/core 0.1.43 → 0.1.45

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.
package/dist/index.cjs CHANGED
@@ -412,6 +412,8 @@ __export(app_settings_exports, {
412
412
  isLinearConnected: () => isLinearConnected,
413
413
  loadAppSettings: () => loadAppSettings,
414
414
  maxConcurrentAgents: () => maxConcurrentAgents,
415
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
416
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
415
417
  resolveClaudeExecutable: () => resolveClaudeExecutable,
416
418
  resolveEffectiveIssueSource: () => resolveEffectiveIssueSource,
417
419
  resolveThreadDefaults: () => resolveThreadDefaults,
@@ -523,6 +525,12 @@ function normalizeAdvanced(raw) {
523
525
  if (typeof source.autoCleanupOrphans === "boolean") {
524
526
  out.autoCleanupOrphans = source.autoCleanupOrphans;
525
527
  }
528
+ if (source.orchestrationQuotaOnLimit === "switch_agent" || source.orchestrationQuotaOnLimit === "wait_reset") {
529
+ out.orchestrationQuotaOnLimit = source.orchestrationQuotaOnLimit;
530
+ }
531
+ if (typeof source.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(source.orchestrationQuotaFallbackAgent)) {
532
+ out.orchestrationQuotaFallbackAgent = source.orchestrationQuotaFallbackAgent;
533
+ }
526
534
  return out;
527
535
  }
528
536
  function normalizeSettings(raw) {
@@ -757,6 +765,12 @@ function updateAdvancedSettings(patch) {
757
765
  if (typeof patch.autoCleanupOrphans === "boolean") {
758
766
  advanced.autoCleanupOrphans = patch.autoCleanupOrphans;
759
767
  }
768
+ if (patch.orchestrationQuotaOnLimit === "switch_agent" || patch.orchestrationQuotaOnLimit === "wait_reset") {
769
+ advanced.orchestrationQuotaOnLimit = patch.orchestrationQuotaOnLimit;
770
+ }
771
+ if (typeof patch.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(patch.orchestrationQuotaFallbackAgent)) {
772
+ advanced.orchestrationQuotaFallbackAgent = patch.orchestrationQuotaFallbackAgent;
773
+ }
760
774
  return saveAppSettings({ ...current, advanced });
761
775
  }
762
776
  function autoRenameBranchEnabled(settings = loadAppSettings()) {
@@ -777,6 +791,13 @@ function deleteBranchOnPurgeEnabled(settings = loadAppSettings()) {
777
791
  function autoCleanupOrphansEnabled(settings = loadAppSettings()) {
778
792
  return Boolean(settings.advanced.autoCleanupOrphans);
779
793
  }
794
+ function orchestrationQuotaOnLimit(settings = loadAppSettings()) {
795
+ return settings.advanced.orchestrationQuotaOnLimit ?? "switch_agent";
796
+ }
797
+ function orchestrationQuotaFallbackAgent(settings = loadAppSettings()) {
798
+ const preferred = settings.advanced.orchestrationQuotaFallbackAgent;
799
+ return preferred && DEFAULT_AGENTS.has(preferred) ? preferred : "cursor";
800
+ }
780
801
  function maxConcurrentAgents(settings = loadAppSettings()) {
781
802
  const n = settings.advanced.maxConcurrent;
782
803
  if (typeof n === "number" && Number.isFinite(n)) {
@@ -893,7 +914,9 @@ function normalizeThread(raw) {
893
914
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
894
915
  prTitle: raw.prTitle ?? null,
895
916
  userSetTitle: Boolean(raw.userSetTitle),
896
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
917
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
918
+ quotaResumeAt: raw.quotaResumeAt ?? null,
919
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
897
920
  };
898
921
  }
899
922
  function createEmptyThread(partial) {
@@ -3184,11 +3207,14 @@ var init_coordinator_prompt = __esm({
3184
3207
  "Discover:",
3185
3208
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3186
3209
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3210
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3187
3211
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3188
3212
  "Workspaces:",
3189
3213
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3190
3214
  "Worktree threads (chats):",
3191
3215
  "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
3216
+ "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
3217
+ "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
3192
3218
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
3193
3219
  "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
3194
3220
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
@@ -3196,8 +3222,9 @@ var init_coordinator_prompt = __esm({
3196
3222
  "Setup / run:",
3197
3223
  "- run_setup \u2014 re-run worktree setup",
3198
3224
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
3199
- "Inspect / PRs:",
3225
+ "Inspect / review / PRs:",
3200
3226
  "- get_diff \u2014 compact diff summary",
3227
+ "- request_review \u2014 open a Review chat tab on a worktree thread (merge-readiness recommendation); then wait_for_turn / get_turn_result on the returned id",
3201
3228
  "- Ask the worktree agent via send_to_thread to open a draft PR with `gh pr create --draft -R <origin-owner/name>` (workspace `github:` slug / that worktree's origin \u2014 never upstream). Do not open PRs from the orchestrator yourself.",
3202
3229
  "Human-only (do not attempt): merge, ready-for-review land, purge_thread.",
3203
3230
  "Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
@@ -3940,11 +3967,15 @@ function humanizeAgentFailDetail(detail) {
3940
3967
  }
3941
3968
  function formatTurnExitError(exitCode, stderrSummary) {
3942
3969
  const code = exitCode ?? 1;
3943
- const detail = humanizeAgentFailDetail(stderrSummary);
3970
+ const raw = stderrSummary.trim();
3971
+ if (/^exit\s*\d+$/i.test(raw)) {
3972
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3973
+ }
3974
+ const detail = humanizeAgentFailDetail(raw);
3944
3975
  if (!detail) {
3945
3976
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3946
3977
  }
3947
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
3978
+ if (looksLikeAgentFailureMessage(raw)) return detail;
3948
3979
  return `exit ${code}: ${detail}`;
3949
3980
  }
3950
3981
  var NODE_VERSION_FOOTER;
@@ -5708,6 +5739,199 @@ var init_opencode = __esm({
5708
5739
  }
5709
5740
  });
5710
5741
 
5742
+ // src/agents/list-models.ts
5743
+ async function listBrightsyModels() {
5744
+ try {
5745
+ const targets = await listBrightsyChatTargets();
5746
+ const accountId = targets.activeAccountId;
5747
+ const models = (targets.models ?? []).map((m) => ({
5748
+ id: encodeBrightsyTarget("model", m.id, accountId),
5749
+ displayName: m.name || m.id,
5750
+ description: m.description ?? void 0
5751
+ }));
5752
+ const agents = (targets.agents ?? []).map((a) => ({
5753
+ id: encodeBrightsyTarget("agent", a.id, accountId),
5754
+ displayName: a.name || a.id,
5755
+ description: a.description ?? "Brightsy agent target"
5756
+ }));
5757
+ return [...models, ...agents].slice(0, 80);
5758
+ } catch {
5759
+ return [];
5760
+ }
5761
+ }
5762
+ async function listModelsForAgent(agent) {
5763
+ const kinds = agent ? [agent] : ["claude", "codex", "opencode", "cursor", "brightsy"];
5764
+ const out = [];
5765
+ for (const kind of kinds) {
5766
+ if (kind === "claude") {
5767
+ out.push({
5768
+ agent: kind,
5769
+ auto: true,
5770
+ models: CLAUDE_MODEL_CATALOG,
5771
+ note: "Default Auto \u2014 only pass a model id when you have a reason."
5772
+ });
5773
+ continue;
5774
+ }
5775
+ if (kind === "codex") {
5776
+ out.push({
5777
+ agent: kind,
5778
+ auto: true,
5779
+ models: await listCodexModels(),
5780
+ note: "Default Auto \u2014 only pass a model slug when you have a reason."
5781
+ });
5782
+ continue;
5783
+ }
5784
+ if (kind === "opencode") {
5785
+ out.push({
5786
+ agent: kind,
5787
+ auto: true,
5788
+ models: await listOpencodeModels(),
5789
+ note: "Default Auto \u2014 only pass a provider/model id when you have a reason."
5790
+ });
5791
+ continue;
5792
+ }
5793
+ if (kind === "cursor") {
5794
+ out.push({
5795
+ agent: kind,
5796
+ auto: true,
5797
+ models: await listCursorModels(),
5798
+ note: 'Default Auto \u2014 only pass a model id when you have a reason (or use "default").'
5799
+ });
5800
+ continue;
5801
+ }
5802
+ if (kind === "brightsy") {
5803
+ const models = await listBrightsyModels();
5804
+ out.push({
5805
+ agent: kind,
5806
+ auto: true,
5807
+ models,
5808
+ note: models.length ? "Default Auto / Default agent \u2014 only pass a model/agent id when you have a reason." : "Brightsy not logged in or no targets \u2014 leave model unset for Default."
5809
+ });
5810
+ }
5811
+ }
5812
+ return out;
5813
+ }
5814
+ var CLAUDE_MODEL_CATALOG;
5815
+ var init_list_models = __esm({
5816
+ "src/agents/list-models.ts"() {
5817
+ "use strict";
5818
+ init_brightsy();
5819
+ init_brightsy_targets();
5820
+ init_codex();
5821
+ init_cursor();
5822
+ init_opencode();
5823
+ CLAUDE_MODEL_CATALOG = [
5824
+ { id: "fable", displayName: "Fable" },
5825
+ { id: "opus", displayName: "Opus" },
5826
+ { id: "sonnet", displayName: "Sonnet" },
5827
+ { id: "haiku", displayName: "Haiku" }
5828
+ ];
5829
+ }
5830
+ });
5831
+
5832
+ // src/agents/session-quota.ts
5833
+ function isSessionQuotaLimit(text) {
5834
+ const lower = text.trim().toLowerCase();
5835
+ if (!lower) return false;
5836
+ if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
5837
+ return false;
5838
+ }
5839
+ if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
5840
+ return false;
5841
+ }
5842
+ return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text);
5843
+ }
5844
+ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
5845
+ const absolute = text.match(
5846
+ /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
5847
+ );
5848
+ if (absolute) {
5849
+ const hour12 = Number(absolute[1]);
5850
+ const minute = Number(absolute[2]);
5851
+ const ampm = absolute[3].toLowerCase();
5852
+ const timeZone = absolute[4]?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
5853
+ let hour = hour12 % 12;
5854
+ if (ampm === "pm") hour += 12;
5855
+ const at = zonedWallTimeToUtc(now, hour, minute, timeZone);
5856
+ if (!at) return null;
5857
+ if (at.getTime() <= now.getTime() + 3e4) {
5858
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
5859
+ return zonedWallTimeToUtc(tomorrow, hour, minute, timeZone);
5860
+ }
5861
+ return at;
5862
+ }
5863
+ const relative = text.match(
5864
+ /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
5865
+ );
5866
+ if (relative) {
5867
+ const n = Number(relative[1]);
5868
+ const unit = relative[2].toLowerCase();
5869
+ const ms = unit.startsWith("day") ? n * 24 * 60 * 60 * 1e3 : unit.startsWith("hour") ? n * 60 * 60 * 1e3 : n * 60 * 1e3;
5870
+ return new Date(now.getTime() + ms);
5871
+ }
5872
+ return null;
5873
+ }
5874
+ function zonedWallTimeToUtc(day, hour, minute, timeZone) {
5875
+ try {
5876
+ const cal = new Intl.DateTimeFormat("en-US", {
5877
+ timeZone,
5878
+ year: "numeric",
5879
+ month: "2-digit",
5880
+ day: "2-digit"
5881
+ });
5882
+ const parts = Object.fromEntries(
5883
+ cal.formatToParts(day).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5884
+ );
5885
+ const year = Number(parts.year);
5886
+ const month = Number(parts.month);
5887
+ const date = Number(parts.day);
5888
+ if (![year, month, date].every((n) => Number.isFinite(n))) return null;
5889
+ const utcGuess = Date.UTC(year, month - 1, date, hour, minute, 0);
5890
+ const dtf = new Intl.DateTimeFormat("en-US", {
5891
+ timeZone,
5892
+ year: "numeric",
5893
+ month: "2-digit",
5894
+ day: "2-digit",
5895
+ hour: "2-digit",
5896
+ minute: "2-digit",
5897
+ second: "2-digit",
5898
+ hourCycle: "h23"
5899
+ });
5900
+ const asParts = Object.fromEntries(
5901
+ dtf.formatToParts(new Date(utcGuess)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5902
+ );
5903
+ const asUtc = Date.UTC(
5904
+ Number(asParts.year),
5905
+ Number(asParts.month) - 1,
5906
+ Number(asParts.day),
5907
+ Number(asParts.hour),
5908
+ Number(asParts.minute),
5909
+ Number(asParts.second || "0")
5910
+ );
5911
+ const offset = asUtc - utcGuess;
5912
+ return new Date(utcGuess - offset);
5913
+ } catch {
5914
+ return null;
5915
+ }
5916
+ }
5917
+ function resolveQuotaFallbackAgent(current, preferred) {
5918
+ const ordered = preferred ? [preferred, ...FALLBACK_ORDER.filter((a) => a !== preferred)] : FALLBACK_ORDER;
5919
+ return ordered.find((a) => a !== current) ?? (current === "cursor" ? "codex" : "cursor");
5920
+ }
5921
+ var FALLBACK_ORDER;
5922
+ var init_session_quota = __esm({
5923
+ "src/agents/session-quota.ts"() {
5924
+ "use strict";
5925
+ FALLBACK_ORDER = [
5926
+ "cursor",
5927
+ "codex",
5928
+ "opencode",
5929
+ "brightsy",
5930
+ "claude"
5931
+ ];
5932
+ }
5933
+ });
5934
+
5711
5935
  // src/agents/install.ts
5712
5936
  function getAgentSetupInfo(agent) {
5713
5937
  return SETUP[agent];
@@ -5888,6 +6112,7 @@ var init_install = __esm({
5888
6112
  // src/agents/index.ts
5889
6113
  var agents_exports = {};
5890
6114
  __export(agents_exports, {
6115
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
5891
6116
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
5892
6117
  allAdapters: () => allAdapters,
5893
6118
  brightsyAdapter: () => brightsyAdapter,
@@ -5902,17 +6127,21 @@ __export(agents_exports, {
5902
6127
  getAgentSetupInfo: () => getAgentSetupInfo,
5903
6128
  installAgent: () => installAgent,
5904
6129
  isCursorAutoModel: () => isCursorAutoModel,
6130
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
5905
6131
  listAgentSetupInfo: () => listAgentSetupInfo,
5906
6132
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5907
6133
  listCodexModels: () => listCodexModels,
5908
6134
  listCursorModels: () => listCursorModels,
6135
+ listModelsForAgent: () => listModelsForAgent,
5909
6136
  listOpencodeModels: () => listOpencodeModels,
5910
6137
  loginAgent: () => loginAgent,
5911
6138
  openInSystemTerminal: () => openInSystemTerminal,
5912
6139
  opencodeAdapter: () => opencodeAdapter,
5913
6140
  parseCursorRunnerLine: () => parseCursorRunnerLine,
6141
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
5914
6142
  permissionMode: () => permissionMode,
5915
- resolveCursorModelId: () => resolveCursorModelId
6143
+ resolveCursorModelId: () => resolveCursorModelId,
6144
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent
5916
6145
  });
5917
6146
  function getAdapter(kind) {
5918
6147
  return adapters[kind];
@@ -5938,6 +6167,8 @@ var init_agents = __esm({
5938
6167
  init_cursor_events();
5939
6168
  init_cursor();
5940
6169
  init_opencode();
6170
+ init_list_models();
6171
+ init_session_quota();
5941
6172
  init_path();
5942
6173
  init_install();
5943
6174
  adapters = {
@@ -5958,10 +6189,10 @@ __export(cursor_recover_exports, {
5958
6189
  function recoverFinishedCursorRun(opts) {
5959
6190
  const agentId = opts.agentId.trim();
5960
6191
  if (!agentId) return null;
5961
- const runsPath = (0, import_node_path23.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
5962
- if (!(0, import_node_fs24.existsSync)(runsPath)) return null;
6192
+ const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
6193
+ if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
5963
6194
  try {
5964
- const lines = (0, import_node_fs24.readFileSync)(runsPath, "utf8").split("\n");
6195
+ const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
5965
6196
  let best = null;
5966
6197
  for (const line of lines) {
5967
6198
  const trimmed = line.trim();
@@ -5987,12 +6218,12 @@ function recoverFinishedCursorRun(opts) {
5987
6218
  return null;
5988
6219
  }
5989
6220
  }
5990
- var import_node_fs24, import_node_path23;
6221
+ var import_node_fs25, import_node_path24;
5991
6222
  var init_cursor_recover = __esm({
5992
6223
  "src/agents/cursor-recover.ts"() {
5993
6224
  "use strict";
5994
- import_node_fs24 = require("fs");
5995
- import_node_path23 = require("path");
6225
+ import_node_fs25 = require("fs");
6226
+ import_node_path24 = require("path");
5996
6227
  init_paths();
5997
6228
  }
5998
6229
  });
@@ -6033,6 +6264,7 @@ var index_exports = {};
6033
6264
  __export(index_exports, {
6034
6265
  BRIGHTSY_MCP_ALLOWED_TOOLS: () => BRIGHTSY_MCP_ALLOWED_TOOLS,
6035
6266
  BrightsySideboardApi: () => BrightsySideboardApi,
6267
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
6036
6268
  CLOUD_COORDINATOR_BUSY_REPLY: () => CLOUD_COORDINATOR_BUSY_REPLY,
6037
6269
  CLOUD_COORDINATOR_STOPPED_REPLY: () => CLOUD_COORDINATOR_STOPPED_REPLY,
6038
6270
  CLOUD_COORDINATOR_TIMEOUT_REPLY: () => CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -6050,6 +6282,9 @@ __export(index_exports, {
6050
6282
  PASTE_ATTACH_MIN_CHARS: () => PASTE_ATTACH_MIN_CHARS,
6051
6283
  PASTE_ATTACH_MIN_LINES: () => PASTE_ATTACH_MIN_LINES,
6052
6284
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
6285
+ REVIEW_REQUEST_NAME: () => REVIEW_REQUEST_NAME,
6286
+ REVIEW_REQUEST_PATH: () => REVIEW_REQUEST_PATH,
6287
+ REVIEW_REQUEST_PREFILL: () => REVIEW_REQUEST_PREFILL,
6053
6288
  SIDEBOARD_FORCE_STOP: () => SIDEBOARD_FORCE_STOP,
6054
6289
  SIDEBOARD_MCP_ALLOWED_TOOLS: () => SIDEBOARD_MCP_ALLOWED_TOOLS,
6055
6290
  THINKING_EFFORTS: () => THINKING_EFFORTS,
@@ -6085,6 +6320,7 @@ __export(index_exports, {
6085
6320
  buildDiffCommentAttachment: () => buildDiffCommentAttachment,
6086
6321
  buildForkTranscriptAttachment: () => buildForkTranscriptAttachment,
6087
6322
  buildPastedTextAttachment: () => buildPastedTextAttachment,
6323
+ buildReviewRequestAttachment: () => buildReviewRequestAttachment,
6088
6324
  buildSessionSeed: () => buildSessionSeed,
6089
6325
  buildWorkspaceScriptEnv: () => buildWorkspaceScriptEnv,
6090
6326
  caffeinateWhileCloudConnectEnabled: () => caffeinateWhileCloudConnectEnabled,
@@ -6205,6 +6441,7 @@ __export(index_exports, {
6205
6441
  isOrchestratorThread: () => isOrchestratorThread,
6206
6442
  isPidAlive: () => isPidAlive,
6207
6443
  isPlaceholderBranch: () => isPlaceholderBranch,
6444
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
6208
6445
  isThinkingEffort: () => isThinkingEffort,
6209
6446
  listAgentSetupInfo: () => listAgentSetupInfo,
6210
6447
  listBranchCommits: () => listBranchCommits,
@@ -6220,6 +6457,7 @@ __export(index_exports, {
6220
6457
  listIssues: () => listIssues,
6221
6458
  listLinearIssues: () => listLinearIssues,
6222
6459
  listLinearIssuesDirect: () => listLinearIssuesDirect,
6460
+ listModelsForAgent: () => listModelsForAgent,
6223
6461
  listOpencodeModels: () => listOpencodeModels,
6224
6462
  listPrs: () => listPrs,
6225
6463
  listRunScripts: () => listRunScripts,
@@ -6251,6 +6489,8 @@ __export(index_exports, {
6251
6489
  normalizeWorktreePath: () => normalizeWorktreePath,
6252
6490
  openInSystemTerminal: () => openInSystemTerminal,
6253
6491
  opencodeAdapter: () => opencodeAdapter,
6492
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
6493
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
6254
6494
  orchestrationTitleNeedsSoccerNickname: () => orchestrationTitleNeedsSoccerNickname,
6255
6495
  orchestratorSessionPoisonedByBuiltins: () => orchestratorSessionPoisonedByBuiltins,
6256
6496
  originGhRepoEnv: () => originGhRepoEnv,
@@ -6258,11 +6498,13 @@ __export(index_exports, {
6258
6498
  parseForceStopMessage: () => parseForceStopMessage,
6259
6499
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
6260
6500
  parseMcpList: () => parseMcpList,
6501
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
6261
6502
  partsToAssistantText: () => partsToAssistantText,
6262
6503
  pastedTextStats: () => pastedTextStats,
6263
6504
  permissionMode: () => permissionMode,
6264
6505
  previewLand: () => previewLand,
6265
6506
  pushBranch: () => pushBranch,
6507
+ readExistingReviewRequestFile: () => readExistingReviewRequestFile,
6266
6508
  readSkillBody: () => readSkillBody,
6267
6509
  readThread: () => readThread,
6268
6510
  readWorktreeFile: () => readWorktreeFile,
@@ -6272,6 +6514,7 @@ __export(index_exports, {
6272
6514
  removeWorkspace: () => removeWorkspace,
6273
6515
  removeWorktree: () => removeWorktree,
6274
6516
  repoSlug: () => repoSlug,
6517
+ requestReview: () => requestReview,
6275
6518
  requireAgent: () => requireAgent,
6276
6519
  resolveClaudeExecutable: () => resolveClaudeExecutable,
6277
6520
  resolveConductorCursorAgentId: () => resolveConductorCursorAgentId,
@@ -6282,6 +6525,7 @@ __export(index_exports, {
6282
6525
  resolveFilesToCopy: () => resolveFilesToCopy,
6283
6526
  resolveGithubRepoSlug: () => resolveGithubRepoSlug,
6284
6527
  resolvePrSelector: () => resolvePrSelector,
6528
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent,
6285
6529
  resolveRepoRoot: () => resolveRepoRoot,
6286
6530
  resolveThreadDefaults: () => resolveThreadDefaults,
6287
6531
  resolveThreadEffort: () => resolveThreadEffort,
@@ -9168,16 +9412,24 @@ function forkChatTab(input) {
9168
9412
  const from = requireThread(input.threadId);
9169
9413
  const slice = forkMessageSlice(from, input.throughIndex);
9170
9414
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9171
- return createChatTab({
9415
+ const tab = createChatTab({
9172
9416
  fromThreadId: input.threadId,
9173
9417
  agent: input.agent ?? from.agent,
9418
+ model: input.model,
9174
9419
  title: input.title?.trim() || void 0,
9175
9420
  attachments: [attachment]
9176
9421
  });
9422
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
9423
+ const next = { ...tab, parentThreadId: from.id };
9424
+ writeThread(next);
9425
+ return next;
9426
+ }
9427
+ return tab;
9177
9428
  }
9178
9429
 
9179
9430
  // src/threads/fork-worktree.ts
9180
9431
  init_thread_store();
9432
+ init_global_workspace();
9181
9433
  function requireThread2(idOrRef) {
9182
9434
  const thread = findThreadByRef(idOrRef) ?? null;
9183
9435
  if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
@@ -9185,16 +9437,28 @@ function requireThread2(idOrRef) {
9185
9437
  }
9186
9438
  async function forkThreadWorktree(input, onSetupLine) {
9187
9439
  const from = requireThread2(input.threadId);
9440
+ if (isOrchestratorThread(from)) {
9441
+ throw new Error(
9442
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
9443
+ );
9444
+ }
9445
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
9446
+ throw new Error(
9447
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
9448
+ );
9449
+ }
9188
9450
  const slice = forkMessageSlice(from, input.throughIndex);
9189
9451
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9452
+ const nextAgent = input.agent ?? from.agent;
9453
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
9190
9454
  const thread = await createThread(
9191
9455
  {
9192
9456
  sourceType: "branch",
9193
9457
  sourceRef: from.branchName,
9194
9458
  repoPath: from.repoPath,
9195
- agent: input.agent ?? from.agent,
9459
+ agent: nextAgent,
9196
9460
  autonomy: from.autonomy,
9197
- model: from.model,
9461
+ model: nextModel,
9198
9462
  effort: from.effort,
9199
9463
  fast: from.fast,
9200
9464
  planMode: from.planMode,
@@ -9491,7 +9755,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
9491
9755
 
9492
9756
  // src/orchestrator/orchestrator.ts
9493
9757
  var import_node_events = require("events");
9494
- var import_node_fs25 = require("fs");
9758
+ var import_node_fs26 = require("fs");
9495
9759
  init_error_detail();
9496
9760
  init_agents();
9497
9761
  init_worktree();
@@ -9735,6 +9999,169 @@ async function cloneRepoIntoSideboard(opts) {
9735
9999
 
9736
10000
  // src/orchestrator/orchestrator.ts
9737
10001
  init_thread_store();
10002
+
10003
+ // src/review/request-review.ts
10004
+ var import_node_crypto5 = require("crypto");
10005
+ var import_node_fs24 = require("fs");
10006
+ var import_node_path23 = require("path");
10007
+ init_global_workspace();
10008
+ init_thread_store();
10009
+ var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
10010
+ var REVIEW_REQUEST_NAME = "Review request.md";
10011
+ var REVIEW_REQUEST_PREFILL = `Please review the changes in this workspace and recommend whether they are ready to merge.
10012
+
10013
+ Start with a **Recommendation**: Approve, Approve with nits, Request changes, or Needs more information \u2014 and say why in 1\u20133 sentences. Then list blocking findings vs nits (findings may be empty).`;
10014
+ function buildReviewRequestAttachment(content) {
10015
+ return {
10016
+ id: (0, import_node_crypto5.randomUUID)(),
10017
+ name: REVIEW_REQUEST_NAME,
10018
+ kind: "file",
10019
+ path: REVIEW_REQUEST_PATH,
10020
+ content
10021
+ };
10022
+ }
10023
+ function readExistingReviewRequestFile(worktreePath) {
10024
+ const abs = (0, import_node_path23.join)(worktreePath, REVIEW_REQUEST_PATH);
10025
+ if (!(0, import_node_fs24.existsSync)(abs)) return null;
10026
+ try {
10027
+ const content = (0, import_node_fs24.readFileSync)(abs, "utf8");
10028
+ return content.trim() ? content : null;
10029
+ } catch {
10030
+ return null;
10031
+ }
10032
+ }
10033
+ async function requestReview(threadRef, send) {
10034
+ const from = findThreadByRef(threadRef);
10035
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
10036
+ if (isOrchestratorThread(from)) {
10037
+ throw new Error(
10038
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
10039
+ );
10040
+ }
10041
+ if (from.status === "archived") {
10042
+ throw new Error(`Thread is archived: ${from.id}`);
10043
+ }
10044
+ const existing = readExistingReviewRequestFile(from.worktreePath);
10045
+ const attachments = existing ? [buildReviewRequestAttachment(existing)] : [];
10046
+ const tab = createChatTab({
10047
+ fromThreadId: from.id,
10048
+ title: "Review",
10049
+ attachments
10050
+ });
10051
+ const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
10052
+ return { tab: started, from };
10053
+ }
10054
+
10055
+ // src/orchestrator/quota-failover.ts
10056
+ var import_node_crypto6 = require("crypto");
10057
+ init_session_quota();
10058
+ init_app_settings();
10059
+ init_global_workspace();
10060
+ init_thread_store();
10061
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
10062
+ if (!isOrchestratorThread(thread)) return null;
10063
+ if (!isSessionQuotaLimit(limitText)) return null;
10064
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
10065
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
10066
+ if (thread.quotaContinuedFromId) {
10067
+ if (resumeAt) {
10068
+ return {
10069
+ action: "wait_reset",
10070
+ reason: "Already continued once; waiting for quota reset instead.",
10071
+ limitText,
10072
+ resumeAt
10073
+ };
10074
+ }
10075
+ return {
10076
+ action: "none",
10077
+ reason: "Already continued once; no parseable reset time.",
10078
+ limitText
10079
+ };
10080
+ }
10081
+ if (onLimit === "wait_reset") {
10082
+ if (!resumeAt) {
10083
+ return {
10084
+ action: "none",
10085
+ reason: "wait_reset configured but reset time could not be parsed.",
10086
+ limitText
10087
+ };
10088
+ }
10089
+ return {
10090
+ action: "wait_reset",
10091
+ reason: "Settings: wait for quota reset.",
10092
+ limitText,
10093
+ resumeAt
10094
+ };
10095
+ }
10096
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
10097
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
10098
+ return {
10099
+ action: "switch_agent",
10100
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
10101
+ limitText,
10102
+ fallbackAgent
10103
+ };
10104
+ }
10105
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
10106
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
10107
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
10108
+ );
10109
+ const recent = from.messages.slice(-8).map((m) => {
10110
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
10111
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
10112
+ return text ? `- ${role}: ${text}` : null;
10113
+ }).filter(Boolean);
10114
+ const body = [
10115
+ `# Orchestration handoff`,
10116
+ "",
10117
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
10118
+ `Limit: ${limitText.trim()}`,
10119
+ `Continuing on **${fallbackAgent}** with Auto model.`,
10120
+ "",
10121
+ `## Goal`,
10122
+ from.sourceRef?.trim() || "(none)",
10123
+ "",
10124
+ `## Child threads`,
10125
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
10126
+ "",
10127
+ `## Recent turns (truncated)`,
10128
+ recent.length ? recent.join("\n") : "(none)",
10129
+ "",
10130
+ `## Instructions`,
10131
+ `- Continue fleet orchestration from this handoff.`,
10132
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
10133
+ `- Leave model Auto unless there is a specific reason to pin one.`,
10134
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
10135
+ ].join("\n");
10136
+ return {
10137
+ id: (0, import_node_crypto6.randomUUID)(),
10138
+ name: "Orchestration quota handoff.md",
10139
+ kind: "transcript",
10140
+ content: body
10141
+ };
10142
+ }
10143
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
10144
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
10145
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
10146
+ ].join(" ");
10147
+ var QUOTA_RESUME_PROMPT = "Session/usage limit window should have reset. Continue the orchestration from where you left off. Use list_threads for fleet status.";
10148
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
10149
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
10150
+ const tab = createChatTab({
10151
+ fromThreadId: from.id,
10152
+ agent: fallbackAgent,
10153
+ model: null,
10154
+ attachments: [handoff]
10155
+ });
10156
+ return updateThread(tab.id, {
10157
+ parentThreadId: from.id,
10158
+ quotaContinuedFromId: from.id,
10159
+ sourceRef: from.sourceRef,
10160
+ sourceType: "orchestration"
10161
+ });
10162
+ }
10163
+
10164
+ // src/orchestrator/orchestrator.ts
9738
10165
  init_types();
9739
10166
  init_settings();
9740
10167
 
@@ -9804,6 +10231,8 @@ var Orchestrator = class {
9804
10231
  haltDrain = /* @__PURE__ */ new Set();
9805
10232
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
9806
10233
  turnBaselines = /* @__PURE__ */ new Map();
10234
+ /** Timers for orchestration session-quota auto-resume. */
10235
+ quotaResumeTimers = /* @__PURE__ */ new Map();
9807
10236
  maxConcurrent;
9808
10237
  runningCount = 0;
9809
10238
  constructor(opts) {
@@ -9853,7 +10282,7 @@ var Orchestrator = class {
9853
10282
  }
9854
10283
  continue;
9855
10284
  }
9856
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
10285
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
9857
10286
  setStatus(thread.id, "broken", "Worktree missing on disk");
9858
10287
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
9859
10288
  continue;
@@ -9883,6 +10312,113 @@ var Orchestrator = class {
9883
10312
  void this.drainQueue(thread.id);
9884
10313
  }
9885
10314
  }
10315
+ this.schedulePendingQuotaResumes();
10316
+ }
10317
+ clearQuotaResumeTimer(threadId) {
10318
+ const timer = this.quotaResumeTimers.get(threadId);
10319
+ if (timer) clearTimeout(timer);
10320
+ this.quotaResumeTimers.delete(threadId);
10321
+ }
10322
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
10323
+ scheduleQuotaResume(threadId, resumeAt) {
10324
+ this.clearQuotaResumeTimer(threadId);
10325
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
10326
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
10327
+ const capped = Math.min(delay, 2147483647);
10328
+ const timer = setTimeout(() => {
10329
+ this.quotaResumeTimers.delete(threadId);
10330
+ void this.resumeAfterQuotaWait(threadId);
10331
+ }, capped);
10332
+ this.quotaResumeTimers.set(threadId, timer);
10333
+ }
10334
+ schedulePendingQuotaResumes() {
10335
+ for (const thread of listThreads({ includeArchived: false })) {
10336
+ if (!thread.quotaResumeAt) continue;
10337
+ const at = new Date(thread.quotaResumeAt);
10338
+ if (Number.isNaN(at.getTime())) continue;
10339
+ if (at.getTime() <= Date.now()) {
10340
+ void this.resumeAfterQuotaWait(thread.id);
10341
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
10342
+ this.scheduleQuotaResume(thread.id, at);
10343
+ }
10344
+ }
10345
+ }
10346
+ async resumeAfterQuotaWait(threadId) {
10347
+ const thread = findThreadByRef(threadId);
10348
+ if (!thread || thread.status === "archived") return;
10349
+ this.clearQuotaResumeTimer(threadId);
10350
+ try {
10351
+ updateThread(threadId, { quotaResumeAt: null });
10352
+ } catch {
10353
+ return;
10354
+ }
10355
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
10356
+ return;
10357
+ }
10358
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
10359
+ }
10360
+ /**
10361
+ * Host-side continue when an orchestration chat hits a provider session/usage
10362
+ * limit (not context size): switch agent (Auto) or wait until reset.
10363
+ */
10364
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
10365
+ const thread = findThreadByRef(threadId);
10366
+ if (!thread) return;
10367
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
10368
+ if (!plan || plan.action === "none") return;
10369
+ if (plan.action === "wait_reset" && plan.resumeAt) {
10370
+ this.haltDrain.add(threadId);
10371
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
10372
+ setStatus(threadId, "idle", null);
10373
+ appendMessage(threadId, {
10374
+ role: "agent",
10375
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
10376
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10377
+ });
10378
+ this.emit({
10379
+ type: "quota_failover",
10380
+ threadId,
10381
+ action: "wait_reset",
10382
+ message: plan.reason,
10383
+ resumeAt: plan.resumeAt.toISOString()
10384
+ });
10385
+ this.emit({ type: "status_changed", threadId, status: "idle" });
10386
+ return;
10387
+ }
10388
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
10389
+ this.haltDrain.add(threadId);
10390
+ const next = createQuotaFailoverChat(
10391
+ thread,
10392
+ plan.fallbackAgent,
10393
+ plan.limitText
10394
+ );
10395
+ this.clearQuotaResumeTimer(threadId);
10396
+ try {
10397
+ updateThread(threadId, { quotaResumeAt: null });
10398
+ } catch {
10399
+ }
10400
+ appendMessage(threadId, {
10401
+ role: "agent",
10402
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
10403
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10404
+ });
10405
+ this.emit({
10406
+ type: "quota_failover",
10407
+ threadId,
10408
+ action: "switch_agent",
10409
+ toThreadId: next.id,
10410
+ message: plan.reason
10411
+ });
10412
+ this.emit({
10413
+ type: "status_changed",
10414
+ threadId: next.id,
10415
+ status: next.status
10416
+ });
10417
+ await this.send(
10418
+ next.id,
10419
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
10420
+ );
10421
+ }
9886
10422
  }
9887
10423
  getThreads(includeArchived = false) {
9888
10424
  return listThreads({ includeArchived });
@@ -10241,11 +10777,16 @@ var Orchestrator = class {
10241
10777
  }
10242
10778
  }
10243
10779
  }
10244
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
10245
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
10780
+ const lastStderr = summarizeTurnStderr(stderrTail);
10781
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10782
+ let chatText = assistantText;
10783
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
10784
+ chatText = humanizeAgentFailDetail(detail);
10785
+ }
10786
+ if (chatText || parts.length > 0) {
10246
10787
  appendMessage(threadId, {
10247
10788
  role: "agent",
10248
- text: assistantText,
10789
+ text: chatText,
10249
10790
  parts: parts.length > 0 ? parts : void 0,
10250
10791
  durationMs: Math.max(0, Date.now() - turnStartedAt),
10251
10792
  usage,
@@ -10264,13 +10805,12 @@ var Orchestrator = class {
10264
10805
  this.emit({ type: "status_changed", threadId, status: "stopped" });
10265
10806
  this.emit({ type: "turn_finished", threadId, exitCode });
10266
10807
  } else {
10267
- const lastStderr = summarizeTurnStderr(stderrTail);
10268
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10269
10808
  const failDetail = formatTurnExitError(exitCode, detail);
10809
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
10270
10810
  setStatus(
10271
10811
  threadId,
10272
10812
  exitCode === 0 ? "idle" : "error",
10273
- exitCode === 0 ? null : failDetail
10813
+ exitCode === 0 || explainedInChat ? null : failDetail
10274
10814
  );
10275
10815
  this.emit({
10276
10816
  type: "status_changed",
@@ -10278,6 +10818,10 @@ var Orchestrator = class {
10278
10818
  status: exitCode === 0 ? "idle" : "error"
10279
10819
  });
10280
10820
  this.emit({ type: "turn_finished", threadId, exitCode });
10821
+ if (exitCode !== 0) {
10822
+ const blob = [chatText, detail].filter(Boolean).join("\n");
10823
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
10824
+ }
10281
10825
  }
10282
10826
  } catch (err) {
10283
10827
  const message = err instanceof Error ? err.message : String(err);
@@ -10291,6 +10835,7 @@ var Orchestrator = class {
10291
10835
  this.emit({ type: "error", threadId, message });
10292
10836
  this.emit({ type: "status_changed", threadId, status: "error" });
10293
10837
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
10838
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
10294
10839
  }
10295
10840
  } finally {
10296
10841
  this.startingTurns.delete(threadId);
@@ -10751,6 +11296,15 @@ var Orchestrator = class {
10751
11296
  setAutonomy(threadRef, autonomy) {
10752
11297
  return this.setThreadOptions(threadRef, { autonomy });
10753
11298
  }
11299
+ /**
11300
+ * Open a Review chat tab on a worktree thread (same as the desktop Review button)
11301
+ * and send the merge-readiness prefill.
11302
+ */
11303
+ async requestReview(threadRef) {
11304
+ const { tab } = await requestReview(threadRef, (ref, prompt) => this.send(ref, prompt));
11305
+ this.emit({ type: "status_changed", threadId: tab.id, status: tab.status });
11306
+ return tab;
11307
+ }
10754
11308
  setThreadOptions(threadRef, patch) {
10755
11309
  const thread = this.requireThread(threadRef);
10756
11310
  const next = {};
@@ -10875,7 +11429,7 @@ var Orchestrator = class {
10875
11429
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
10876
11430
  return setStatus(thread.id, "idle");
10877
11431
  }
10878
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
11432
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
10879
11433
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
10880
11434
  const { execa: execa7 } = await import("execa");
10881
11435
  const slug = thread.worktreePath.split("/").pop();
@@ -10989,9 +11543,10 @@ init_coordinator_prompt();
10989
11543
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
10990
11544
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
10991
11545
  var import_zod = require("zod");
10992
- var import_node_path24 = require("path");
11546
+ var import_node_path25 = require("path");
10993
11547
  init_worktree();
10994
11548
  init_global_workspace();
11549
+ init_list_models();
10995
11550
 
10996
11551
  // src/mcp/archive-guard.ts
10997
11552
  init_global_workspace();
@@ -11037,7 +11592,7 @@ async function startMcpServer() {
11037
11592
  async () => {
11038
11593
  const threads = orch.getThreads(true);
11039
11594
  const lines = threads.map((t) => {
11040
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path24.basename)(t.repoPath) || t.repoPath;
11595
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
11041
11596
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
11042
11597
  });
11043
11598
  return {
@@ -11380,6 +11935,148 @@ async function startMcpServer() {
11380
11935
  };
11381
11936
  }
11382
11937
  );
11938
+ server.tool(
11939
+ "request_review",
11940
+ "Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, optionally attaches .sideboard/attachments/Review request.md when present, and asks for Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.",
11941
+ { ref: import_zod.z.string().describe("Worktree thread id/ref to review") },
11942
+ async ({ ref }) => {
11943
+ try {
11944
+ const tab = await orch.requestReview(ref);
11945
+ const from = orch.getThread(ref);
11946
+ return {
11947
+ content: [
11948
+ {
11949
+ type: "text",
11950
+ text: JSON.stringify({
11951
+ id: tab.id,
11952
+ title: tab.title,
11953
+ status: tab.status,
11954
+ fromThreadId: from?.id ?? ref,
11955
+ link: `sideboard://thread/${tab.id}`
11956
+ })
11957
+ }
11958
+ ]
11959
+ };
11960
+ } catch (err) {
11961
+ const message = err instanceof Error ? err.message : String(err);
11962
+ return { content: [{ type: "text", text: message }], isError: true };
11963
+ }
11964
+ }
11965
+ );
11966
+ const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
11967
+ server.tool(
11968
+ "list_models",
11969
+ "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
11970
+ {
11971
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
11972
+ },
11973
+ async ({ agent }) => {
11974
+ try {
11975
+ const catalogs = await listModelsForAgent(agent);
11976
+ return {
11977
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
11978
+ };
11979
+ } catch (err) {
11980
+ const message = err instanceof Error ? err.message : String(err);
11981
+ return { content: [{ type: "text", text: message }], isError: true };
11982
+ }
11983
+ }
11984
+ );
11985
+ server.tool(
11986
+ "fork_worktree",
11987
+ "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
11988
+ {
11989
+ ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
11990
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11991
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11992
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11993
+ title: import_zod.z.string().optional()
11994
+ },
11995
+ async ({ ref, through_index, agent, model, title }) => {
11996
+ try {
11997
+ const source = orch.getThread(ref);
11998
+ if (source) await orch.reconcile(source.repoPath);
11999
+ const thread = await orch.forkThreadWorktree({
12000
+ threadId: ref,
12001
+ throughIndex: through_index,
12002
+ agent,
12003
+ model,
12004
+ title
12005
+ });
12006
+ return {
12007
+ content: [
12008
+ {
12009
+ type: "text",
12010
+ text: JSON.stringify({
12011
+ id: thread.id,
12012
+ title: thread.title,
12013
+ status: thread.status,
12014
+ agent: thread.agent,
12015
+ model: thread.model,
12016
+ branchName: thread.branchName,
12017
+ worktreePath: thread.worktreePath,
12018
+ fromThreadId: source?.id ?? ref,
12019
+ link: `sideboard://thread/${thread.id}`
12020
+ })
12021
+ }
12022
+ ]
12023
+ };
12024
+ } catch (err) {
12025
+ const message = err instanceof Error ? err.message : String(err);
12026
+ return { content: [{ type: "text", text: message }], isError: true };
12027
+ }
12028
+ }
12029
+ );
12030
+ server.tool(
12031
+ "fork_chat",
12032
+ "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Remote coordinators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
12033
+ {
12034
+ ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
12035
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
12036
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
12037
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
12038
+ title: import_zod.z.string().optional()
12039
+ },
12040
+ async ({ ref, through_index, agent, model, title }) => {
12041
+ try {
12042
+ const source = orch.getThread(ref);
12043
+ if (!source) {
12044
+ return {
12045
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
12046
+ isError: true
12047
+ };
12048
+ }
12049
+ const tab = orch.forkChatTab({
12050
+ threadId: source.id,
12051
+ throughIndex: through_index,
12052
+ agent,
12053
+ model,
12054
+ title
12055
+ });
12056
+ return {
12057
+ content: [
12058
+ {
12059
+ type: "text",
12060
+ text: JSON.stringify({
12061
+ id: tab.id,
12062
+ title: tab.title,
12063
+ status: tab.status,
12064
+ agent: tab.agent,
12065
+ model: tab.model,
12066
+ sourceType: tab.sourceType,
12067
+ worktreePath: tab.worktreePath,
12068
+ fromThreadId: source.id,
12069
+ link: `sideboard://thread/${tab.id}`
12070
+ })
12071
+ }
12072
+ ]
12073
+ };
12074
+ } catch (err) {
12075
+ const message = err instanceof Error ? err.message : String(err);
12076
+ return { content: [{ type: "text", text: message }], isError: true };
12077
+ }
12078
+ }
12079
+ );
11383
12080
  server.tool(
11384
12081
  "run_dev_script",
11385
12082
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
@@ -11906,6 +12603,7 @@ init_injected_mcp();
11906
12603
  0 && (module.exports = {
11907
12604
  BRIGHTSY_MCP_ALLOWED_TOOLS,
11908
12605
  BrightsySideboardApi,
12606
+ CLAUDE_MODEL_CATALOG,
11909
12607
  CLOUD_COORDINATOR_BUSY_REPLY,
11910
12608
  CLOUD_COORDINATOR_STOPPED_REPLY,
11911
12609
  CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -11923,6 +12621,9 @@ init_injected_mcp();
11923
12621
  PASTE_ATTACH_MIN_CHARS,
11924
12622
  PASTE_ATTACH_MIN_LINES,
11925
12623
  PLAN_MODE_INSTRUCTION,
12624
+ REVIEW_REQUEST_NAME,
12625
+ REVIEW_REQUEST_PATH,
12626
+ REVIEW_REQUEST_PREFILL,
11926
12627
  SIDEBOARD_FORCE_STOP,
11927
12628
  SIDEBOARD_MCP_ALLOWED_TOOLS,
11928
12629
  THINKING_EFFORTS,
@@ -11958,6 +12659,7 @@ init_injected_mcp();
11958
12659
  buildDiffCommentAttachment,
11959
12660
  buildForkTranscriptAttachment,
11960
12661
  buildPastedTextAttachment,
12662
+ buildReviewRequestAttachment,
11961
12663
  buildSessionSeed,
11962
12664
  buildWorkspaceScriptEnv,
11963
12665
  caffeinateWhileCloudConnectEnabled,
@@ -12078,6 +12780,7 @@ init_injected_mcp();
12078
12780
  isOrchestratorThread,
12079
12781
  isPidAlive,
12080
12782
  isPlaceholderBranch,
12783
+ isSessionQuotaLimit,
12081
12784
  isThinkingEffort,
12082
12785
  listAgentSetupInfo,
12083
12786
  listBranchCommits,
@@ -12093,6 +12796,7 @@ init_injected_mcp();
12093
12796
  listIssues,
12094
12797
  listLinearIssues,
12095
12798
  listLinearIssuesDirect,
12799
+ listModelsForAgent,
12096
12800
  listOpencodeModels,
12097
12801
  listPrs,
12098
12802
  listRunScripts,
@@ -12124,6 +12828,8 @@ init_injected_mcp();
12124
12828
  normalizeWorktreePath,
12125
12829
  openInSystemTerminal,
12126
12830
  opencodeAdapter,
12831
+ orchestrationQuotaFallbackAgent,
12832
+ orchestrationQuotaOnLimit,
12127
12833
  orchestrationTitleNeedsSoccerNickname,
12128
12834
  orchestratorSessionPoisonedByBuiltins,
12129
12835
  originGhRepoEnv,
@@ -12131,11 +12837,13 @@ init_injected_mcp();
12131
12837
  parseForceStopMessage,
12132
12838
  parseGithubSlugFromRemoteUrl,
12133
12839
  parseMcpList,
12840
+ parseSessionQuotaResetAt,
12134
12841
  partsToAssistantText,
12135
12842
  pastedTextStats,
12136
12843
  permissionMode,
12137
12844
  previewLand,
12138
12845
  pushBranch,
12846
+ readExistingReviewRequestFile,
12139
12847
  readSkillBody,
12140
12848
  readThread,
12141
12849
  readWorktreeFile,
@@ -12145,6 +12853,7 @@ init_injected_mcp();
12145
12853
  removeWorkspace,
12146
12854
  removeWorktree,
12147
12855
  repoSlug,
12856
+ requestReview,
12148
12857
  requireAgent,
12149
12858
  resolveClaudeExecutable,
12150
12859
  resolveConductorCursorAgentId,
@@ -12155,6 +12864,7 @@ init_injected_mcp();
12155
12864
  resolveFilesToCopy,
12156
12865
  resolveGithubRepoSlug,
12157
12866
  resolvePrSelector,
12867
+ resolveQuotaFallbackAgent,
12158
12868
  resolveRepoRoot,
12159
12869
  resolveThreadDefaults,
12160
12870
  resolveThreadEffort,