@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.
@@ -133,11 +133,15 @@ function humanizeAgentFailDetail(detail) {
133
133
  }
134
134
  function formatTurnExitError(exitCode, stderrSummary) {
135
135
  const code = exitCode ?? 1;
136
- const detail = humanizeAgentFailDetail(stderrSummary);
136
+ const raw = stderrSummary.trim();
137
+ if (/^exit\s*\d+$/i.test(raw)) {
138
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
139
+ }
140
+ const detail = humanizeAgentFailDetail(raw);
137
141
  if (!detail) {
138
142
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
139
143
  }
140
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
144
+ if (looksLikeAgentFailureMessage(raw)) return detail;
141
145
  return `exit ${code}: ${detail}`;
142
146
  }
143
147
  var NODE_VERSION_FOOTER;
@@ -477,7 +481,9 @@ function normalizeThread(raw) {
477
481
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
478
482
  prTitle: raw.prTitle ?? null,
479
483
  userSetTitle: Boolean(raw.userSetTitle),
480
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
484
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
485
+ quotaResumeAt: raw.quotaResumeAt ?? null,
486
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
481
487
  };
482
488
  }
483
489
  function createEmptyThread(partial) {
@@ -2631,6 +2637,8 @@ __export(app_settings_exports, {
2631
2637
  isLinearConnected: () => isLinearConnected,
2632
2638
  loadAppSettings: () => loadAppSettings,
2633
2639
  maxConcurrentAgents: () => maxConcurrentAgents,
2640
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
2641
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
2634
2642
  resolveClaudeExecutable: () => resolveClaudeExecutable,
2635
2643
  resolveEffectiveIssueSource: () => resolveEffectiveIssueSource,
2636
2644
  resolveThreadDefaults: () => resolveThreadDefaults,
@@ -2742,6 +2750,12 @@ function normalizeAdvanced(raw) {
2742
2750
  if (typeof source.autoCleanupOrphans === "boolean") {
2743
2751
  out.autoCleanupOrphans = source.autoCleanupOrphans;
2744
2752
  }
2753
+ if (source.orchestrationQuotaOnLimit === "switch_agent" || source.orchestrationQuotaOnLimit === "wait_reset") {
2754
+ out.orchestrationQuotaOnLimit = source.orchestrationQuotaOnLimit;
2755
+ }
2756
+ if (typeof source.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(source.orchestrationQuotaFallbackAgent)) {
2757
+ out.orchestrationQuotaFallbackAgent = source.orchestrationQuotaFallbackAgent;
2758
+ }
2745
2759
  return out;
2746
2760
  }
2747
2761
  function normalizeSettings(raw) {
@@ -2976,6 +2990,12 @@ function updateAdvancedSettings(patch) {
2976
2990
  if (typeof patch.autoCleanupOrphans === "boolean") {
2977
2991
  advanced.autoCleanupOrphans = patch.autoCleanupOrphans;
2978
2992
  }
2993
+ if (patch.orchestrationQuotaOnLimit === "switch_agent" || patch.orchestrationQuotaOnLimit === "wait_reset") {
2994
+ advanced.orchestrationQuotaOnLimit = patch.orchestrationQuotaOnLimit;
2995
+ }
2996
+ if (typeof patch.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(patch.orchestrationQuotaFallbackAgent)) {
2997
+ advanced.orchestrationQuotaFallbackAgent = patch.orchestrationQuotaFallbackAgent;
2998
+ }
2979
2999
  return saveAppSettings({ ...current, advanced });
2980
3000
  }
2981
3001
  function autoRenameBranchEnabled(settings = loadAppSettings()) {
@@ -2996,6 +3016,13 @@ function deleteBranchOnPurgeEnabled(settings = loadAppSettings()) {
2996
3016
  function autoCleanupOrphansEnabled(settings = loadAppSettings()) {
2997
3017
  return Boolean(settings.advanced.autoCleanupOrphans);
2998
3018
  }
3019
+ function orchestrationQuotaOnLimit(settings = loadAppSettings()) {
3020
+ return settings.advanced.orchestrationQuotaOnLimit ?? "switch_agent";
3021
+ }
3022
+ function orchestrationQuotaFallbackAgent(settings = loadAppSettings()) {
3023
+ const preferred = settings.advanced.orchestrationQuotaFallbackAgent;
3024
+ return preferred && DEFAULT_AGENTS.has(preferred) ? preferred : "cursor";
3025
+ }
2999
3026
  function maxConcurrentAgents(settings = loadAppSettings()) {
3000
3027
  const n = settings.advanced.maxConcurrent;
3001
3028
  if (typeof n === "number" && Number.isFinite(n)) {
@@ -3223,11 +3250,14 @@ var init_coordinator_prompt = __esm({
3223
3250
  "Discover:",
3224
3251
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3225
3252
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3253
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3226
3254
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3227
3255
  "Workspaces:",
3228
3256
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3229
3257
  "Worktree threads (chats):",
3230
3258
  "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
3259
+ "- 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.",
3260
+ "- 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.",
3231
3261
  "- 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",
3232
3262
  "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
3233
3263
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
@@ -3235,8 +3265,9 @@ var init_coordinator_prompt = __esm({
3235
3265
  "Setup / run:",
3236
3266
  "- run_setup \u2014 re-run worktree setup",
3237
3267
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
3238
- "Inspect / PRs:",
3268
+ "Inspect / review / PRs:",
3239
3269
  "- get_diff \u2014 compact diff summary",
3270
+ "- 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",
3240
3271
  "- 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.",
3241
3272
  "Human-only (do not attempt): merge, ready-for-review land, purge_thread.",
3242
3273
  "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.",
@@ -5321,6 +5352,199 @@ var init_opencode = __esm({
5321
5352
  }
5322
5353
  });
5323
5354
 
5355
+ // src/agents/list-models.ts
5356
+ async function listBrightsyModels() {
5357
+ try {
5358
+ const targets = await listBrightsyChatTargets();
5359
+ const accountId = targets.activeAccountId;
5360
+ const models = (targets.models ?? []).map((m) => ({
5361
+ id: encodeBrightsyTarget("model", m.id, accountId),
5362
+ displayName: m.name || m.id,
5363
+ description: m.description ?? void 0
5364
+ }));
5365
+ const agents = (targets.agents ?? []).map((a) => ({
5366
+ id: encodeBrightsyTarget("agent", a.id, accountId),
5367
+ displayName: a.name || a.id,
5368
+ description: a.description ?? "Brightsy agent target"
5369
+ }));
5370
+ return [...models, ...agents].slice(0, 80);
5371
+ } catch {
5372
+ return [];
5373
+ }
5374
+ }
5375
+ async function listModelsForAgent(agent) {
5376
+ const kinds = agent ? [agent] : ["claude", "codex", "opencode", "cursor", "brightsy"];
5377
+ const out = [];
5378
+ for (const kind of kinds) {
5379
+ if (kind === "claude") {
5380
+ out.push({
5381
+ agent: kind,
5382
+ auto: true,
5383
+ models: CLAUDE_MODEL_CATALOG,
5384
+ note: "Default Auto \u2014 only pass a model id when you have a reason."
5385
+ });
5386
+ continue;
5387
+ }
5388
+ if (kind === "codex") {
5389
+ out.push({
5390
+ agent: kind,
5391
+ auto: true,
5392
+ models: await listCodexModels(),
5393
+ note: "Default Auto \u2014 only pass a model slug when you have a reason."
5394
+ });
5395
+ continue;
5396
+ }
5397
+ if (kind === "opencode") {
5398
+ out.push({
5399
+ agent: kind,
5400
+ auto: true,
5401
+ models: await listOpencodeModels(),
5402
+ note: "Default Auto \u2014 only pass a provider/model id when you have a reason."
5403
+ });
5404
+ continue;
5405
+ }
5406
+ if (kind === "cursor") {
5407
+ out.push({
5408
+ agent: kind,
5409
+ auto: true,
5410
+ models: await listCursorModels(),
5411
+ note: 'Default Auto \u2014 only pass a model id when you have a reason (or use "default").'
5412
+ });
5413
+ continue;
5414
+ }
5415
+ if (kind === "brightsy") {
5416
+ const models = await listBrightsyModels();
5417
+ out.push({
5418
+ agent: kind,
5419
+ auto: true,
5420
+ models,
5421
+ 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."
5422
+ });
5423
+ }
5424
+ }
5425
+ return out;
5426
+ }
5427
+ var CLAUDE_MODEL_CATALOG;
5428
+ var init_list_models = __esm({
5429
+ "src/agents/list-models.ts"() {
5430
+ "use strict";
5431
+ init_brightsy();
5432
+ init_brightsy_targets();
5433
+ init_codex();
5434
+ init_cursor();
5435
+ init_opencode();
5436
+ CLAUDE_MODEL_CATALOG = [
5437
+ { id: "fable", displayName: "Fable" },
5438
+ { id: "opus", displayName: "Opus" },
5439
+ { id: "sonnet", displayName: "Sonnet" },
5440
+ { id: "haiku", displayName: "Haiku" }
5441
+ ];
5442
+ }
5443
+ });
5444
+
5445
+ // src/agents/session-quota.ts
5446
+ function isSessionQuotaLimit(text) {
5447
+ const lower = text.trim().toLowerCase();
5448
+ if (!lower) return false;
5449
+ if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
5450
+ return false;
5451
+ }
5452
+ if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
5453
+ return false;
5454
+ }
5455
+ 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);
5456
+ }
5457
+ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
5458
+ const absolute = text.match(
5459
+ /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
5460
+ );
5461
+ if (absolute) {
5462
+ const hour12 = Number(absolute[1]);
5463
+ const minute = Number(absolute[2]);
5464
+ const ampm = absolute[3].toLowerCase();
5465
+ const timeZone = absolute[4]?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
5466
+ let hour = hour12 % 12;
5467
+ if (ampm === "pm") hour += 12;
5468
+ const at = zonedWallTimeToUtc(now, hour, minute, timeZone);
5469
+ if (!at) return null;
5470
+ if (at.getTime() <= now.getTime() + 3e4) {
5471
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
5472
+ return zonedWallTimeToUtc(tomorrow, hour, minute, timeZone);
5473
+ }
5474
+ return at;
5475
+ }
5476
+ const relative = text.match(
5477
+ /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
5478
+ );
5479
+ if (relative) {
5480
+ const n = Number(relative[1]);
5481
+ const unit = relative[2].toLowerCase();
5482
+ const ms = unit.startsWith("day") ? n * 24 * 60 * 60 * 1e3 : unit.startsWith("hour") ? n * 60 * 60 * 1e3 : n * 60 * 1e3;
5483
+ return new Date(now.getTime() + ms);
5484
+ }
5485
+ return null;
5486
+ }
5487
+ function zonedWallTimeToUtc(day, hour, minute, timeZone) {
5488
+ try {
5489
+ const cal = new Intl.DateTimeFormat("en-US", {
5490
+ timeZone,
5491
+ year: "numeric",
5492
+ month: "2-digit",
5493
+ day: "2-digit"
5494
+ });
5495
+ const parts = Object.fromEntries(
5496
+ cal.formatToParts(day).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5497
+ );
5498
+ const year = Number(parts.year);
5499
+ const month = Number(parts.month);
5500
+ const date = Number(parts.day);
5501
+ if (![year, month, date].every((n) => Number.isFinite(n))) return null;
5502
+ const utcGuess = Date.UTC(year, month - 1, date, hour, minute, 0);
5503
+ const dtf = new Intl.DateTimeFormat("en-US", {
5504
+ timeZone,
5505
+ year: "numeric",
5506
+ month: "2-digit",
5507
+ day: "2-digit",
5508
+ hour: "2-digit",
5509
+ minute: "2-digit",
5510
+ second: "2-digit",
5511
+ hourCycle: "h23"
5512
+ });
5513
+ const asParts = Object.fromEntries(
5514
+ dtf.formatToParts(new Date(utcGuess)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5515
+ );
5516
+ const asUtc = Date.UTC(
5517
+ Number(asParts.year),
5518
+ Number(asParts.month) - 1,
5519
+ Number(asParts.day),
5520
+ Number(asParts.hour),
5521
+ Number(asParts.minute),
5522
+ Number(asParts.second || "0")
5523
+ );
5524
+ const offset = asUtc - utcGuess;
5525
+ return new Date(utcGuess - offset);
5526
+ } catch {
5527
+ return null;
5528
+ }
5529
+ }
5530
+ function resolveQuotaFallbackAgent(current, preferred) {
5531
+ const ordered = preferred ? [preferred, ...FALLBACK_ORDER.filter((a) => a !== preferred)] : FALLBACK_ORDER;
5532
+ return ordered.find((a) => a !== current) ?? (current === "cursor" ? "codex" : "cursor");
5533
+ }
5534
+ var FALLBACK_ORDER;
5535
+ var init_session_quota = __esm({
5536
+ "src/agents/session-quota.ts"() {
5537
+ "use strict";
5538
+ FALLBACK_ORDER = [
5539
+ "cursor",
5540
+ "codex",
5541
+ "opencode",
5542
+ "brightsy",
5543
+ "claude"
5544
+ ];
5545
+ }
5546
+ });
5547
+
5324
5548
  // src/agents/install.ts
5325
5549
  function getAgentSetupInfo(agent) {
5326
5550
  return SETUP[agent];
@@ -5501,6 +5725,7 @@ var init_install = __esm({
5501
5725
  // src/agents/index.ts
5502
5726
  var agents_exports = {};
5503
5727
  __export(agents_exports, {
5728
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
5504
5729
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
5505
5730
  allAdapters: () => allAdapters,
5506
5731
  brightsyAdapter: () => brightsyAdapter,
@@ -5515,17 +5740,21 @@ __export(agents_exports, {
5515
5740
  getAgentSetupInfo: () => getAgentSetupInfo,
5516
5741
  installAgent: () => installAgent,
5517
5742
  isCursorAutoModel: () => isCursorAutoModel,
5743
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
5518
5744
  listAgentSetupInfo: () => listAgentSetupInfo,
5519
5745
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5520
5746
  listCodexModels: () => listCodexModels,
5521
5747
  listCursorModels: () => listCursorModels,
5748
+ listModelsForAgent: () => listModelsForAgent,
5522
5749
  listOpencodeModels: () => listOpencodeModels,
5523
5750
  loginAgent: () => loginAgent,
5524
5751
  openInSystemTerminal: () => openInSystemTerminal,
5525
5752
  opencodeAdapter: () => opencodeAdapter,
5526
5753
  parseCursorRunnerLine: () => parseCursorRunnerLine,
5754
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
5527
5755
  permissionMode: () => permissionMode,
5528
- resolveCursorModelId: () => resolveCursorModelId
5756
+ resolveCursorModelId: () => resolveCursorModelId,
5757
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent
5529
5758
  });
5530
5759
  function getAdapter(kind) {
5531
5760
  return adapters[kind];
@@ -5551,6 +5780,8 @@ var init_agents = __esm({
5551
5780
  init_cursor_events();
5552
5781
  init_cursor();
5553
5782
  init_opencode();
5783
+ init_list_models();
5784
+ init_session_quota();
5554
5785
  init_path();
5555
5786
  init_install();
5556
5787
  adapters = {
@@ -5690,10 +5921,10 @@ __export(cursor_recover_exports, {
5690
5921
  function recoverFinishedCursorRun(opts) {
5691
5922
  const agentId = opts.agentId.trim();
5692
5923
  if (!agentId) return null;
5693
- const runsPath = (0, import_node_path23.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
5694
- if (!(0, import_node_fs24.existsSync)(runsPath)) return null;
5924
+ const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
5925
+ if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
5695
5926
  try {
5696
- const lines = (0, import_node_fs24.readFileSync)(runsPath, "utf8").split("\n");
5927
+ const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
5697
5928
  let best = null;
5698
5929
  for (const line of lines) {
5699
5930
  const trimmed = line.trim();
@@ -5719,12 +5950,12 @@ function recoverFinishedCursorRun(opts) {
5719
5950
  return null;
5720
5951
  }
5721
5952
  }
5722
- var import_node_fs24, import_node_path23;
5953
+ var import_node_fs25, import_node_path24;
5723
5954
  var init_cursor_recover = __esm({
5724
5955
  "src/agents/cursor-recover.ts"() {
5725
5956
  "use strict";
5726
- import_node_fs24 = require("fs");
5727
- import_node_path23 = require("path");
5957
+ import_node_fs25 = require("fs");
5958
+ import_node_path24 = require("path");
5728
5959
  init_paths();
5729
5960
  }
5730
5961
  });
@@ -5733,11 +5964,11 @@ var init_cursor_recover = __esm({
5733
5964
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5734
5965
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5735
5966
  var import_zod = require("zod");
5736
- var import_node_path24 = require("path");
5967
+ var import_node_path25 = require("path");
5737
5968
 
5738
5969
  // src/orchestrator/orchestrator.ts
5739
5970
  var import_node_events = require("events");
5740
- var import_node_fs25 = require("fs");
5971
+ var import_node_fs26 = require("fs");
5741
5972
  init_error_detail();
5742
5973
 
5743
5974
  // src/agents/spawn.ts
@@ -7174,16 +7405,76 @@ function forkChatTab(input) {
7174
7405
  const from = requireThread(input.threadId);
7175
7406
  const slice = forkMessageSlice(from, input.throughIndex);
7176
7407
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
7177
- return createChatTab({
7408
+ const tab = createChatTab({
7178
7409
  fromThreadId: input.threadId,
7179
7410
  agent: input.agent ?? from.agent,
7411
+ model: input.model,
7180
7412
  title: input.title?.trim() || void 0,
7181
7413
  attachments: [attachment]
7182
7414
  });
7415
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
7416
+ const next = { ...tab, parentThreadId: from.id };
7417
+ writeThread(next);
7418
+ return next;
7419
+ }
7420
+ return tab;
7421
+ }
7422
+
7423
+ // src/review/request-review.ts
7424
+ var import_node_crypto3 = require("crypto");
7425
+ var import_node_fs19 = require("fs");
7426
+ var import_node_path18 = require("path");
7427
+ init_global_workspace();
7428
+ init_thread_store();
7429
+ var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
7430
+ var REVIEW_REQUEST_NAME = "Review request.md";
7431
+ var REVIEW_REQUEST_PREFILL = `Please review the changes in this workspace and recommend whether they are ready to merge.
7432
+
7433
+ 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).`;
7434
+ function buildReviewRequestAttachment(content) {
7435
+ return {
7436
+ id: (0, import_node_crypto3.randomUUID)(),
7437
+ name: REVIEW_REQUEST_NAME,
7438
+ kind: "file",
7439
+ path: REVIEW_REQUEST_PATH,
7440
+ content
7441
+ };
7442
+ }
7443
+ function readExistingReviewRequestFile(worktreePath) {
7444
+ const abs = (0, import_node_path18.join)(worktreePath, REVIEW_REQUEST_PATH);
7445
+ if (!(0, import_node_fs19.existsSync)(abs)) return null;
7446
+ try {
7447
+ const content = (0, import_node_fs19.readFileSync)(abs, "utf8");
7448
+ return content.trim() ? content : null;
7449
+ } catch {
7450
+ return null;
7451
+ }
7452
+ }
7453
+ async function requestReview(threadRef, send) {
7454
+ const from = findThreadByRef(threadRef);
7455
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
7456
+ if (isOrchestratorThread(from)) {
7457
+ throw new Error(
7458
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
7459
+ );
7460
+ }
7461
+ if (from.status === "archived") {
7462
+ throw new Error(`Thread is archived: ${from.id}`);
7463
+ }
7464
+ const existing = readExistingReviewRequestFile(from.worktreePath);
7465
+ const attachments = existing ? [buildReviewRequestAttachment(existing)] : [];
7466
+ const tab = createChatTab({
7467
+ fromThreadId: from.id,
7468
+ title: "Review",
7469
+ attachments
7470
+ });
7471
+ const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
7472
+ return { tab: started, from };
7183
7473
  }
7184
7474
 
7185
7475
  // src/threads/fork-worktree.ts
7186
7476
  init_thread_store();
7477
+ init_global_workspace();
7187
7478
  function requireThread2(idOrRef) {
7188
7479
  const thread = findThreadByRef(idOrRef) ?? null;
7189
7480
  if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
@@ -7191,16 +7482,28 @@ function requireThread2(idOrRef) {
7191
7482
  }
7192
7483
  async function forkThreadWorktree(input, onSetupLine) {
7193
7484
  const from = requireThread2(input.threadId);
7485
+ if (isOrchestratorThread(from)) {
7486
+ throw new Error(
7487
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
7488
+ );
7489
+ }
7490
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
7491
+ throw new Error(
7492
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
7493
+ );
7494
+ }
7194
7495
  const slice = forkMessageSlice(from, input.throughIndex);
7195
7496
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
7497
+ const nextAgent = input.agent ?? from.agent;
7498
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
7196
7499
  const thread = await createThread(
7197
7500
  {
7198
7501
  sourceType: "branch",
7199
7502
  sourceRef: from.branchName,
7200
7503
  repoPath: from.repoPath,
7201
- agent: input.agent ?? from.agent,
7504
+ agent: nextAgent,
7202
7505
  autonomy: from.autonomy,
7203
- model: from.model,
7506
+ model: nextModel,
7204
7507
  effort: from.effort,
7205
7508
  fast: from.fast,
7206
7509
  planMode: from.planMode,
@@ -7213,22 +7516,131 @@ async function forkThreadWorktree(input, onSetupLine) {
7213
7516
  return thread;
7214
7517
  }
7215
7518
 
7519
+ // src/orchestrator/quota-failover.ts
7520
+ var import_node_crypto4 = require("crypto");
7521
+ init_session_quota();
7522
+ init_app_settings();
7523
+ init_global_workspace();
7524
+ init_thread_store();
7525
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
7526
+ if (!isOrchestratorThread(thread)) return null;
7527
+ if (!isSessionQuotaLimit(limitText)) return null;
7528
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
7529
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
7530
+ if (thread.quotaContinuedFromId) {
7531
+ if (resumeAt) {
7532
+ return {
7533
+ action: "wait_reset",
7534
+ reason: "Already continued once; waiting for quota reset instead.",
7535
+ limitText,
7536
+ resumeAt
7537
+ };
7538
+ }
7539
+ return {
7540
+ action: "none",
7541
+ reason: "Already continued once; no parseable reset time.",
7542
+ limitText
7543
+ };
7544
+ }
7545
+ if (onLimit === "wait_reset") {
7546
+ if (!resumeAt) {
7547
+ return {
7548
+ action: "none",
7549
+ reason: "wait_reset configured but reset time could not be parsed.",
7550
+ limitText
7551
+ };
7552
+ }
7553
+ return {
7554
+ action: "wait_reset",
7555
+ reason: "Settings: wait for quota reset.",
7556
+ limitText,
7557
+ resumeAt
7558
+ };
7559
+ }
7560
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
7561
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
7562
+ return {
7563
+ action: "switch_agent",
7564
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
7565
+ limitText,
7566
+ fallbackAgent
7567
+ };
7568
+ }
7569
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
7570
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
7571
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
7572
+ );
7573
+ const recent = from.messages.slice(-8).map((m) => {
7574
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
7575
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
7576
+ return text ? `- ${role}: ${text}` : null;
7577
+ }).filter(Boolean);
7578
+ const body = [
7579
+ `# Orchestration handoff`,
7580
+ "",
7581
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
7582
+ `Limit: ${limitText.trim()}`,
7583
+ `Continuing on **${fallbackAgent}** with Auto model.`,
7584
+ "",
7585
+ `## Goal`,
7586
+ from.sourceRef?.trim() || "(none)",
7587
+ "",
7588
+ `## Child threads`,
7589
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
7590
+ "",
7591
+ `## Recent turns (truncated)`,
7592
+ recent.length ? recent.join("\n") : "(none)",
7593
+ "",
7594
+ `## Instructions`,
7595
+ `- Continue fleet orchestration from this handoff.`,
7596
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
7597
+ `- Leave model Auto unless there is a specific reason to pin one.`,
7598
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
7599
+ ].join("\n");
7600
+ return {
7601
+ id: (0, import_node_crypto4.randomUUID)(),
7602
+ name: "Orchestration quota handoff.md",
7603
+ kind: "transcript",
7604
+ content: body
7605
+ };
7606
+ }
7607
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
7608
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
7609
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
7610
+ ].join(" ");
7611
+ 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.";
7612
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
7613
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
7614
+ const tab = createChatTab({
7615
+ fromThreadId: from.id,
7616
+ agent: fallbackAgent,
7617
+ model: null,
7618
+ attachments: [handoff]
7619
+ });
7620
+ return updateThread(tab.id, {
7621
+ parentThreadId: from.id,
7622
+ quotaContinuedFromId: from.id,
7623
+ sourceRef: from.sourceRef,
7624
+ sourceType: "orchestration"
7625
+ });
7626
+ }
7627
+
7216
7628
  // src/threads/adopt.ts
7217
7629
  var import_node_child_process = require("child_process");
7218
- var import_node_fs19 = require("fs");
7630
+ var import_node_fs20 = require("fs");
7219
7631
  var import_node_os8 = require("os");
7220
- var import_node_path18 = require("path");
7632
+ var import_node_path19 = require("path");
7221
7633
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
7222
7634
  init_worktree();
7223
7635
  init_thread_store();
7224
- var CONDUCTOR_APP_SUPPORT = (0, import_node_path18.join)(
7636
+ var CONDUCTOR_APP_SUPPORT = (0, import_node_path19.join)(
7225
7637
  process.env.HOME ?? "",
7226
7638
  "Library",
7227
7639
  "Application Support",
7228
7640
  "com.conductor.app"
7229
7641
  );
7230
- var CONDUCTOR_DB = (0, import_node_path18.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
7231
- var CURSOR_SDK_STORE = (0, import_node_path18.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
7642
+ var CONDUCTOR_DB = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
7643
+ var CURSOR_SDK_STORE = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
7232
7644
  function mapAgentType(raw) {
7233
7645
  if (!raw) return null;
7234
7646
  const v = raw.toLowerCase();
@@ -7240,21 +7652,21 @@ function mapAgentType(raw) {
7240
7652
  return null;
7241
7653
  }
7242
7654
  function resolveConductorCursorAgentId(workspacePath) {
7243
- if (!workspacePath || !(0, import_node_fs19.existsSync)(CURSOR_SDK_STORE)) return null;
7655
+ if (!workspacePath || !(0, import_node_fs20.existsSync)(CURSOR_SDK_STORE)) return null;
7244
7656
  const normalized = workspacePath.replace(/\/$/, "");
7245
7657
  let best = null;
7246
7658
  let hashes;
7247
7659
  try {
7248
- hashes = (0, import_node_fs19.readdirSync)(CURSOR_SDK_STORE);
7660
+ hashes = (0, import_node_fs20.readdirSync)(CURSOR_SDK_STORE);
7249
7661
  } catch {
7250
7662
  return null;
7251
7663
  }
7252
7664
  for (const hash of hashes) {
7253
- const agentsFile = (0, import_node_path18.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
7254
- if (!(0, import_node_fs19.existsSync)(agentsFile)) continue;
7665
+ const agentsFile = (0, import_node_path19.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
7666
+ if (!(0, import_node_fs20.existsSync)(agentsFile)) continue;
7255
7667
  let text;
7256
7668
  try {
7257
- text = (0, import_node_fs19.readFileSync)(agentsFile, "utf8");
7669
+ text = (0, import_node_fs20.readFileSync)(agentsFile, "utf8");
7258
7670
  } catch {
7259
7671
  continue;
7260
7672
  }
@@ -7278,7 +7690,7 @@ function resolveConductorCursorAgentId(workspacePath) {
7278
7690
  return best?.agentId ?? null;
7279
7691
  }
7280
7692
  async function adoptThread(input) {
7281
- if (!(0, import_node_fs19.existsSync)(input.worktreePath)) {
7693
+ if (!(0, import_node_fs20.existsSync)(input.worktreePath)) {
7282
7694
  throw new Error(`Worktree not found: ${input.worktreePath}`);
7283
7695
  }
7284
7696
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -7302,18 +7714,18 @@ async function adoptThread(input) {
7302
7714
  return thread;
7303
7715
  }
7304
7716
  function listConductorWorkspaces() {
7305
- if (!(0, import_node_fs19.existsSync)(CONDUCTOR_DB)) {
7717
+ if (!(0, import_node_fs20.existsSync)(CONDUCTOR_DB)) {
7306
7718
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
7307
7719
  }
7308
- const tmp = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7309
- const snapshot = (0, import_node_path18.join)(tmp, "conductor.db");
7720
+ const tmp = (0, import_node_fs20.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7721
+ const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
7310
7722
  try {
7311
- (0, import_node_fs19.copyFileSync)(CONDUCTOR_DB, snapshot);
7723
+ (0, import_node_fs20.copyFileSync)(CONDUCTOR_DB, snapshot);
7312
7724
  for (const suffix of ["-wal", "-shm"]) {
7313
7725
  const src = `${CONDUCTOR_DB}${suffix}`;
7314
- if ((0, import_node_fs19.existsSync)(src)) {
7726
+ if ((0, import_node_fs20.existsSync)(src)) {
7315
7727
  try {
7316
- (0, import_node_fs19.copyFileSync)(src, `${snapshot}${suffix}`);
7728
+ (0, import_node_fs20.copyFileSync)(src, `${snapshot}${suffix}`);
7317
7729
  } catch {
7318
7730
  }
7319
7731
  }
@@ -7389,22 +7801,22 @@ function listConductorWorkspaces() {
7389
7801
  db.close();
7390
7802
  }
7391
7803
  } finally {
7392
- (0, import_node_fs19.rmSync)(tmp, { recursive: true, force: true });
7804
+ (0, import_node_fs20.rmSync)(tmp, { recursive: true, force: true });
7393
7805
  }
7394
7806
  }
7395
7807
  function importConductorWorkspace(workspaceId) {
7396
- if (!(0, import_node_fs19.existsSync)(CONDUCTOR_DB)) {
7808
+ if (!(0, import_node_fs20.existsSync)(CONDUCTOR_DB)) {
7397
7809
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
7398
7810
  }
7399
- const tmp = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7400
- const snapshot = (0, import_node_path18.join)(tmp, "conductor.db");
7811
+ const tmp = (0, import_node_fs20.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7812
+ const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
7401
7813
  try {
7402
- (0, import_node_fs19.copyFileSync)(CONDUCTOR_DB, snapshot);
7814
+ (0, import_node_fs20.copyFileSync)(CONDUCTOR_DB, snapshot);
7403
7815
  for (const suffix of ["-wal", "-shm"]) {
7404
7816
  const src = `${CONDUCTOR_DB}${suffix}`;
7405
- if ((0, import_node_fs19.existsSync)(src)) {
7817
+ if ((0, import_node_fs20.existsSync)(src)) {
7406
7818
  try {
7407
- (0, import_node_fs19.copyFileSync)(src, `${snapshot}${suffix}`);
7819
+ (0, import_node_fs20.copyFileSync)(src, `${snapshot}${suffix}`);
7408
7820
  } catch {
7409
7821
  }
7410
7822
  }
@@ -7422,7 +7834,7 @@ function importConductorWorkspace(workspaceId) {
7422
7834
  ).get(workspaceId);
7423
7835
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
7424
7836
  const worktreePath = String(row.workspacePath);
7425
- if (!(0, import_node_fs19.existsSync)(worktreePath)) {
7837
+ if (!(0, import_node_fs20.existsSync)(worktreePath)) {
7426
7838
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
7427
7839
  }
7428
7840
  let sessionId = null;
@@ -7485,7 +7897,7 @@ function importConductorWorkspace(workspaceId) {
7485
7897
  db.close();
7486
7898
  }
7487
7899
  } finally {
7488
- (0, import_node_fs19.rmSync)(tmp, { recursive: true, force: true });
7900
+ (0, import_node_fs20.rmSync)(tmp, { recursive: true, force: true });
7489
7901
  }
7490
7902
  }
7491
7903
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -7496,12 +7908,12 @@ async function importConductorWorkspaceAsync(workspaceId) {
7496
7908
  init_worktree();
7497
7909
 
7498
7910
  // src/diff/diff.ts
7499
- var import_node_fs20 = require("fs");
7500
- var import_node_path19 = require("path");
7911
+ var import_node_fs21 = require("fs");
7912
+ var import_node_path20 = require("path");
7501
7913
  init_run();
7502
7914
  init_worktree();
7503
7915
  async function inspectGitWorktree(worktreePath) {
7504
- if (!worktreePath || !(0, import_node_fs20.existsSync)(worktreePath)) return "missing_worktree";
7916
+ if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) return "missing_worktree";
7505
7917
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
7506
7918
  reject: false
7507
7919
  });
@@ -7509,7 +7921,7 @@ async function inspectGitWorktree(worktreePath) {
7509
7921
  return "ok";
7510
7922
  }
7511
7923
  async function initializeGitRepository(worktreePath) {
7512
- if (!worktreePath || !(0, import_node_fs20.existsSync)(worktreePath)) {
7924
+ if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) {
7513
7925
  throw new Error("Worktree not found");
7514
7926
  }
7515
7927
  const status = await inspectGitWorktree(worktreePath);
@@ -7941,8 +8353,8 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
7941
8353
  function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7942
8354
  assertSafeRelativePath(relativePath);
7943
8355
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
7944
- const abs = (0, import_node_path19.join)(worktreePath, relativePath);
7945
- const st = (0, import_node_fs20.statSync)(abs);
8356
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8357
+ const st = (0, import_node_fs21.statSync)(abs);
7946
8358
  if (!st.isFile()) {
7947
8359
  throw new Error(`Not a file: ${relativePath}`);
7948
8360
  }
@@ -7951,7 +8363,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7951
8363
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
7952
8364
  );
7953
8365
  }
7954
- const buf = (0, import_node_fs20.readFileSync)(abs);
8366
+ const buf = (0, import_node_fs21.readFileSync)(abs);
7955
8367
  return {
7956
8368
  path: relativePath,
7957
8369
  contentBase64: buf.toString("base64"),
@@ -7961,12 +8373,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7961
8373
  function readWorktreeFile(worktreePath, relativePath, opts) {
7962
8374
  assertSafeRelativePath(relativePath);
7963
8375
  const maxBytes = opts?.maxBytes ?? 2e5;
7964
- const abs = (0, import_node_path19.join)(worktreePath, relativePath);
7965
- const st = (0, import_node_fs20.statSync)(abs);
8376
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8377
+ const st = (0, import_node_fs21.statSync)(abs);
7966
8378
  if (!st.isFile()) {
7967
8379
  throw new Error(`Not a file: ${relativePath}`);
7968
8380
  }
7969
- const buf = (0, import_node_fs20.readFileSync)(abs);
8381
+ const buf = (0, import_node_fs21.readFileSync)(abs);
7970
8382
  if (isImageRelativePath(relativePath)) {
7971
8383
  const maxImageBytes = Math.max(maxBytes, 15e6);
7972
8384
  const truncated2 = buf.length > maxImageBytes;
@@ -8009,9 +8421,9 @@ function assertSafeRelativePath(relativePath) {
8009
8421
  }
8010
8422
  function writeWorktreeFile(worktreePath, relativePath, content) {
8011
8423
  assertSafeRelativePath(relativePath);
8012
- const abs = (0, import_node_path19.join)(worktreePath, relativePath);
8013
- (0, import_node_fs20.mkdirSync)((0, import_node_path19.dirname)(abs), { recursive: true });
8014
- (0, import_node_fs20.writeFileSync)(abs, content, "utf8");
8424
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8425
+ (0, import_node_fs21.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
8426
+ (0, import_node_fs21.writeFileSync)(abs, content, "utf8");
8015
8427
  return { path: relativePath };
8016
8428
  }
8017
8429
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -8190,9 +8602,9 @@ async function confirmLand(thread, opts) {
8190
8602
  }
8191
8603
 
8192
8604
  // src/skills/discover.ts
8193
- var import_node_fs21 = require("fs");
8605
+ var import_node_fs22 = require("fs");
8194
8606
  var import_node_os9 = require("os");
8195
- var import_node_path20 = require("path");
8607
+ var import_node_path21 = require("path");
8196
8608
  function toCommand(name) {
8197
8609
  return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8198
8610
  }
@@ -8224,7 +8636,7 @@ function parseFrontmatter(content) {
8224
8636
  }
8225
8637
  function readSkill(skillMd, source) {
8226
8638
  try {
8227
- const content = (0, import_node_fs21.readFileSync)(skillMd, "utf8");
8639
+ const content = (0, import_node_fs22.readFileSync)(skillMd, "utf8");
8228
8640
  const { name: fmName, description } = parseFrontmatter(content);
8229
8641
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
8230
8642
  const name = fmName || dirName;
@@ -8243,19 +8655,19 @@ function readSkill(skillMd, source) {
8243
8655
  }
8244
8656
  }
8245
8657
  function scanSkillsDir(dir, source, out) {
8246
- if (!(0, import_node_fs21.existsSync)(dir)) return;
8658
+ if (!(0, import_node_fs22.existsSync)(dir)) return;
8247
8659
  let entries;
8248
8660
  try {
8249
- entries = (0, import_node_fs21.readdirSync)(dir);
8661
+ entries = (0, import_node_fs22.readdirSync)(dir);
8250
8662
  } catch {
8251
8663
  return;
8252
8664
  }
8253
8665
  for (const entry of entries) {
8254
8666
  if (entry.startsWith(".")) continue;
8255
- const skillMd = (0, import_node_path20.join)(dir, entry, "SKILL.md");
8256
- if (!(0, import_node_fs21.existsSync)(skillMd)) continue;
8667
+ const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
8668
+ if (!(0, import_node_fs22.existsSync)(skillMd)) continue;
8257
8669
  try {
8258
- if (!(0, import_node_fs21.statSync)(skillMd).isFile()) continue;
8670
+ if (!(0, import_node_fs22.statSync)(skillMd).isFile()) continue;
8259
8671
  } catch {
8260
8672
  continue;
8261
8673
  }
@@ -8264,24 +8676,24 @@ function scanSkillsDir(dir, source, out) {
8264
8676
  }
8265
8677
  }
8266
8678
  function scanClaudePluginSkills(pluginsRoot, out) {
8267
- if (!(0, import_node_fs21.existsSync)(pluginsRoot)) return;
8679
+ if (!(0, import_node_fs22.existsSync)(pluginsRoot)) return;
8268
8680
  const walk = (dir, depth, lookingForSkillsDir) => {
8269
8681
  if (depth > 7) return;
8270
8682
  let entries;
8271
8683
  try {
8272
- entries = (0, import_node_fs21.readdirSync)(dir);
8684
+ entries = (0, import_node_fs22.readdirSync)(dir);
8273
8685
  } catch {
8274
8686
  return;
8275
8687
  }
8276
8688
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
8277
- const skill = readSkill((0, import_node_path20.join)(dir, "SKILL.md"), "cli");
8689
+ const skill = readSkill((0, import_node_path21.join)(dir, "SKILL.md"), "cli");
8278
8690
  if (skill) out.push(skill);
8279
8691
  }
8280
8692
  for (const entry of entries) {
8281
8693
  if (entry === "node_modules" || entry === ".git") continue;
8282
- const full = (0, import_node_path20.join)(dir, entry);
8694
+ const full = (0, import_node_path21.join)(dir, entry);
8283
8695
  try {
8284
- if (!(0, import_node_fs21.statSync)(full).isDirectory()) continue;
8696
+ if (!(0, import_node_fs22.statSync)(full).isDirectory()) continue;
8285
8697
  } catch {
8286
8698
  continue;
8287
8699
  }
@@ -8299,17 +8711,17 @@ function discoverSkills(worktreePath) {
8299
8711
  const home = (0, import_node_os9.homedir)();
8300
8712
  const collected = [];
8301
8713
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
8302
- scanSkillsDir((0, import_node_path20.join)(worktreePath, rel), "workspace", collected);
8714
+ scanSkillsDir((0, import_node_path21.join)(worktreePath, rel), "workspace", collected);
8303
8715
  }
8304
8716
  for (const abs of [
8305
- (0, import_node_path20.join)(home, ".claude/skills"),
8306
- (0, import_node_path20.join)(home, ".cursor/skills"),
8307
- (0, import_node_path20.join)(home, ".sideboard/skills"),
8308
- (0, import_node_path20.join)(home, ".brightsy/skills")
8717
+ (0, import_node_path21.join)(home, ".claude/skills"),
8718
+ (0, import_node_path21.join)(home, ".cursor/skills"),
8719
+ (0, import_node_path21.join)(home, ".sideboard/skills"),
8720
+ (0, import_node_path21.join)(home, ".brightsy/skills")
8309
8721
  ]) {
8310
8722
  scanSkillsDir(abs, "user", collected);
8311
8723
  }
8312
- scanClaudePluginSkills((0, import_node_path20.join)(home, ".claude/plugins"), collected);
8724
+ scanClaudePluginSkills((0, import_node_path21.join)(home, ".claude/plugins"), collected);
8313
8725
  const rank = { workspace: 0, user: 1, cli: 2 };
8314
8726
  const byCommand = /* @__PURE__ */ new Map();
8315
8727
  for (const skill of collected) {
@@ -8321,7 +8733,7 @@ function discoverSkills(worktreePath) {
8321
8733
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
8322
8734
  }
8323
8735
  function readSkillBody(skillPath, maxChars = 12e3) {
8324
- const raw = (0, import_node_fs21.readFileSync)(skillPath, "utf8");
8736
+ const raw = (0, import_node_fs22.readFileSync)(skillPath, "utf8");
8325
8737
  if (raw.startsWith("---")) {
8326
8738
  const end = raw.indexOf("\n---", 3);
8327
8739
  if (end >= 0) {
@@ -8414,9 +8826,9 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
8414
8826
  }
8415
8827
 
8416
8828
  // src/composer/stage-files.ts
8417
- var import_node_fs22 = require("fs");
8418
- var import_node_path21 = require("path");
8419
- var import_node_crypto3 = require("crypto");
8829
+ var import_node_fs23 = require("fs");
8830
+ var import_node_path22 = require("path");
8831
+ var import_node_crypto5 = require("crypto");
8420
8832
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
8421
8833
  "png",
8422
8834
  "jpg",
@@ -8445,7 +8857,7 @@ var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local on
8445
8857
  var MAX_INLINE_BYTES = 4e5;
8446
8858
  var MAX_PREVIEW_BYTES = 5e6;
8447
8859
  function fileExtension(filePath) {
8448
- const base = (0, import_node_path21.basename)(filePath).toLowerCase();
8860
+ const base = (0, import_node_path22.basename)(filePath).toLowerCase();
8449
8861
  return base.includes(".") ? base.split(".").pop() || "" : "";
8450
8862
  }
8451
8863
  function isImageFilePath(filePath) {
@@ -8455,24 +8867,24 @@ function imageMimeType(filePath) {
8455
8867
  return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
8456
8868
  }
8457
8869
  function ensureAttachmentsDir(worktreePath) {
8458
- const dir = (0, import_node_path21.join)(worktreePath, ATTACHMENTS_DIR);
8459
- (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
8460
- const gi = (0, import_node_path21.join)(dir, ".gitignore");
8461
- if (!(0, import_node_fs22.existsSync)(gi)) {
8462
- (0, import_node_fs22.writeFileSync)(gi, ATTACHMENTS_GITIGNORE, "utf8");
8870
+ const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
8871
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
8872
+ const gi = (0, import_node_path22.join)(dir, ".gitignore");
8873
+ if (!(0, import_node_fs23.existsSync)(gi)) {
8874
+ (0, import_node_fs23.writeFileSync)(gi, ATTACHMENTS_GITIGNORE, "utf8");
8463
8875
  }
8464
8876
  return dir;
8465
8877
  }
8466
8878
  function uniqueAttachmentName(dir, originalName) {
8467
8879
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
8468
- if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, safe))) return safe;
8469
- const ext = (0, import_node_path21.extname)(safe);
8880
+ if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
8881
+ const ext = (0, import_node_path22.extname)(safe);
8470
8882
  const stem = ext ? safe.slice(0, -ext.length) : safe;
8471
8883
  for (let i = 1; i < 1e4; i++) {
8472
8884
  const candidate = `${stem}-${i}${ext}`;
8473
- if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, candidate))) return candidate;
8885
+ if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
8474
8886
  }
8475
- return `${stem}-${(0, import_node_crypto3.randomUUID)()}${ext}`;
8887
+ return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
8476
8888
  }
8477
8889
  function previewDataUrlFromBuf(filePath, buf) {
8478
8890
  if (!isImageFilePath(filePath)) return void 0;
@@ -8484,7 +8896,7 @@ function attachmentFromBuffer(name, buf, opts) {
8484
8896
  if (isImageFilePath(name)) {
8485
8897
  const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
8486
8898
  return {
8487
- id: (0, import_node_crypto3.randomUUID)(),
8899
+ id: (0, import_node_crypto5.randomUUID)(),
8488
8900
  name,
8489
8901
  kind: "file",
8490
8902
  path: opts.path,
@@ -8497,7 +8909,7 @@ function attachmentFromBuffer(name, buf, opts) {
8497
8909
  }
8498
8910
  if (buf.length > MAX_INLINE_BYTES) {
8499
8911
  return {
8500
- id: (0, import_node_crypto3.randomUUID)(),
8912
+ id: (0, import_node_crypto5.randomUUID)(),
8501
8913
  name,
8502
8914
  kind: "file",
8503
8915
  path: opts.path,
@@ -8506,7 +8918,7 @@ function attachmentFromBuffer(name, buf, opts) {
8506
8918
  }
8507
8919
  if (buf.includes(0)) {
8508
8920
  return {
8509
- id: (0, import_node_crypto3.randomUUID)(),
8921
+ id: (0, import_node_crypto5.randomUUID)(),
8510
8922
  name,
8511
8923
  kind: "file",
8512
8924
  path: opts.path,
@@ -8514,7 +8926,7 @@ function attachmentFromBuffer(name, buf, opts) {
8514
8926
  };
8515
8927
  }
8516
8928
  return {
8517
- id: (0, import_node_crypto3.randomUUID)(),
8929
+ id: (0, import_node_crypto5.randomUUID)(),
8518
8930
  name,
8519
8931
  kind: "file",
8520
8932
  path: opts.path,
@@ -8526,19 +8938,19 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
8526
8938
  const dir = ensureAttachmentsDir(worktreePath);
8527
8939
  const out = [];
8528
8940
  for (const abs of absolutePaths) {
8529
- const originalName = (0, import_node_path21.basename)(abs);
8941
+ const originalName = (0, import_node_path22.basename)(abs);
8530
8942
  try {
8531
- const st = (0, import_node_fs22.statSync)(abs);
8943
+ const st = (0, import_node_fs23.statSync)(abs);
8532
8944
  if (!st.isFile()) continue;
8533
8945
  const name = uniqueAttachmentName(dir, originalName);
8534
- const destAbs = (0, import_node_path21.join)(dir, name);
8535
- (0, import_node_fs22.copyFileSync)(abs, destAbs);
8946
+ const destAbs = (0, import_node_path22.join)(dir, name);
8947
+ (0, import_node_fs23.copyFileSync)(abs, destAbs);
8536
8948
  const rel = `${ATTACHMENTS_DIR}/${name}`;
8537
- const buf = (0, import_node_fs22.readFileSync)(destAbs);
8949
+ const buf = (0, import_node_fs23.readFileSync)(destAbs);
8538
8950
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8539
8951
  } catch (err) {
8540
8952
  out.push({
8541
- id: (0, import_node_crypto3.randomUUID)(),
8953
+ id: (0, import_node_crypto5.randomUUID)(),
8542
8954
  name: originalName,
8543
8955
  kind: "file",
8544
8956
  content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
@@ -8556,13 +8968,13 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
8556
8968
  try {
8557
8969
  const buf = Buffer.from(item.dataBase64, "base64");
8558
8970
  const name = uniqueAttachmentName(dir, originalName);
8559
- const destAbs = (0, import_node_path21.join)(dir, name);
8560
- (0, import_node_fs22.writeFileSync)(destAbs, buf);
8971
+ const destAbs = (0, import_node_path22.join)(dir, name);
8972
+ (0, import_node_fs23.writeFileSync)(destAbs, buf);
8561
8973
  const rel = `${ATTACHMENTS_DIR}/${name}`;
8562
8974
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
8563
8975
  } catch (err) {
8564
8976
  out.push({
8565
- id: (0, import_node_crypto3.randomUUID)(),
8977
+ id: (0, import_node_crypto5.randomUUID)(),
8566
8978
  name: originalName,
8567
8979
  kind: "file",
8568
8980
  content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
@@ -8576,23 +8988,23 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8576
8988
  for (const rel of relativePaths) {
8577
8989
  if (!rel || rel.includes("..") || rel.startsWith("/")) {
8578
8990
  out.push({
8579
- id: (0, import_node_crypto3.randomUUID)(),
8580
- name: (0, import_node_path21.basename)(rel) || "file",
8991
+ id: (0, import_node_crypto5.randomUUID)(),
8992
+ name: (0, import_node_path22.basename)(rel) || "file",
8581
8993
  kind: "file",
8582
8994
  content: `(invalid path: ${rel})`
8583
8995
  });
8584
8996
  continue;
8585
8997
  }
8586
- const name = (0, import_node_path21.basename)(rel);
8998
+ const name = (0, import_node_path22.basename)(rel);
8587
8999
  try {
8588
- const abs = (0, import_node_path21.join)(worktreePath, rel);
8589
- const st = (0, import_node_fs22.statSync)(abs);
9000
+ const abs = (0, import_node_path22.join)(worktreePath, rel);
9001
+ const st = (0, import_node_fs23.statSync)(abs);
8590
9002
  if (!st.isFile()) continue;
8591
- const buf = (0, import_node_fs22.readFileSync)(abs);
9003
+ const buf = (0, import_node_fs23.readFileSync)(abs);
8592
9004
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8593
9005
  } catch (err) {
8594
9006
  out.push({
8595
- id: (0, import_node_crypto3.randomUUID)(),
9007
+ id: (0, import_node_crypto5.randomUUID)(),
8596
9008
  name,
8597
9009
  kind: "file",
8598
9010
  content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
@@ -8603,8 +9015,8 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8603
9015
  }
8604
9016
 
8605
9017
  // src/agents/instructions.ts
8606
- var import_node_fs23 = require("fs");
8607
- var import_node_path22 = require("path");
9018
+ var import_node_fs24 = require("fs");
9019
+ var import_node_path23 = require("path");
8608
9020
  init_worktree_labels();
8609
9021
  function normPath2(p) {
8610
9022
  return p.replace(/\/+$/, "");
@@ -8743,11 +9155,11 @@ function loadAgentInstructions(worktreePath, agent) {
8743
9155
  const out = [];
8744
9156
  for (const rel of candidates) {
8745
9157
  if (seen.has(rel)) continue;
8746
- const abs = (0, import_node_path22.join)(worktreePath, rel);
8747
- if (!(0, import_node_fs23.existsSync)(abs)) continue;
9158
+ const abs = (0, import_node_path23.join)(worktreePath, rel);
9159
+ if (!(0, import_node_fs24.existsSync)(abs)) continue;
8748
9160
  try {
8749
- if (!(0, import_node_fs23.statSync)(abs).isFile()) continue;
8750
- let content = (0, import_node_fs23.readFileSync)(abs, "utf8");
9161
+ if (!(0, import_node_fs24.statSync)(abs).isFile()) continue;
9162
+ let content = (0, import_node_fs24.readFileSync)(abs, "utf8");
8751
9163
  if (!content.trim()) continue;
8752
9164
  if (content.length > MAX_CHARS_PER_FILE) {
8753
9165
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -8848,6 +9260,8 @@ var Orchestrator = class {
8848
9260
  haltDrain = /* @__PURE__ */ new Set();
8849
9261
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
8850
9262
  turnBaselines = /* @__PURE__ */ new Map();
9263
+ /** Timers for orchestration session-quota auto-resume. */
9264
+ quotaResumeTimers = /* @__PURE__ */ new Map();
8851
9265
  maxConcurrent;
8852
9266
  runningCount = 0;
8853
9267
  constructor(opts) {
@@ -8897,7 +9311,7 @@ var Orchestrator = class {
8897
9311
  }
8898
9312
  continue;
8899
9313
  }
8900
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
9314
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
8901
9315
  setStatus(thread.id, "broken", "Worktree missing on disk");
8902
9316
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
8903
9317
  continue;
@@ -8927,6 +9341,113 @@ var Orchestrator = class {
8927
9341
  void this.drainQueue(thread.id);
8928
9342
  }
8929
9343
  }
9344
+ this.schedulePendingQuotaResumes();
9345
+ }
9346
+ clearQuotaResumeTimer(threadId) {
9347
+ const timer = this.quotaResumeTimers.get(threadId);
9348
+ if (timer) clearTimeout(timer);
9349
+ this.quotaResumeTimers.delete(threadId);
9350
+ }
9351
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
9352
+ scheduleQuotaResume(threadId, resumeAt) {
9353
+ this.clearQuotaResumeTimer(threadId);
9354
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
9355
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
9356
+ const capped = Math.min(delay, 2147483647);
9357
+ const timer = setTimeout(() => {
9358
+ this.quotaResumeTimers.delete(threadId);
9359
+ void this.resumeAfterQuotaWait(threadId);
9360
+ }, capped);
9361
+ this.quotaResumeTimers.set(threadId, timer);
9362
+ }
9363
+ schedulePendingQuotaResumes() {
9364
+ for (const thread of listThreads({ includeArchived: false })) {
9365
+ if (!thread.quotaResumeAt) continue;
9366
+ const at = new Date(thread.quotaResumeAt);
9367
+ if (Number.isNaN(at.getTime())) continue;
9368
+ if (at.getTime() <= Date.now()) {
9369
+ void this.resumeAfterQuotaWait(thread.id);
9370
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
9371
+ this.scheduleQuotaResume(thread.id, at);
9372
+ }
9373
+ }
9374
+ }
9375
+ async resumeAfterQuotaWait(threadId) {
9376
+ const thread = findThreadByRef(threadId);
9377
+ if (!thread || thread.status === "archived") return;
9378
+ this.clearQuotaResumeTimer(threadId);
9379
+ try {
9380
+ updateThread(threadId, { quotaResumeAt: null });
9381
+ } catch {
9382
+ return;
9383
+ }
9384
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
9385
+ return;
9386
+ }
9387
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
9388
+ }
9389
+ /**
9390
+ * Host-side continue when an orchestration chat hits a provider session/usage
9391
+ * limit (not context size): switch agent (Auto) or wait until reset.
9392
+ */
9393
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
9394
+ const thread = findThreadByRef(threadId);
9395
+ if (!thread) return;
9396
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
9397
+ if (!plan || plan.action === "none") return;
9398
+ if (plan.action === "wait_reset" && plan.resumeAt) {
9399
+ this.haltDrain.add(threadId);
9400
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
9401
+ setStatus(threadId, "idle", null);
9402
+ appendMessage(threadId, {
9403
+ role: "agent",
9404
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
9405
+ ts: (/* @__PURE__ */ new Date()).toISOString()
9406
+ });
9407
+ this.emit({
9408
+ type: "quota_failover",
9409
+ threadId,
9410
+ action: "wait_reset",
9411
+ message: plan.reason,
9412
+ resumeAt: plan.resumeAt.toISOString()
9413
+ });
9414
+ this.emit({ type: "status_changed", threadId, status: "idle" });
9415
+ return;
9416
+ }
9417
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
9418
+ this.haltDrain.add(threadId);
9419
+ const next = createQuotaFailoverChat(
9420
+ thread,
9421
+ plan.fallbackAgent,
9422
+ plan.limitText
9423
+ );
9424
+ this.clearQuotaResumeTimer(threadId);
9425
+ try {
9426
+ updateThread(threadId, { quotaResumeAt: null });
9427
+ } catch {
9428
+ }
9429
+ appendMessage(threadId, {
9430
+ role: "agent",
9431
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
9432
+ ts: (/* @__PURE__ */ new Date()).toISOString()
9433
+ });
9434
+ this.emit({
9435
+ type: "quota_failover",
9436
+ threadId,
9437
+ action: "switch_agent",
9438
+ toThreadId: next.id,
9439
+ message: plan.reason
9440
+ });
9441
+ this.emit({
9442
+ type: "status_changed",
9443
+ threadId: next.id,
9444
+ status: next.status
9445
+ });
9446
+ await this.send(
9447
+ next.id,
9448
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
9449
+ );
9450
+ }
8930
9451
  }
8931
9452
  getThreads(includeArchived = false) {
8932
9453
  return listThreads({ includeArchived });
@@ -9285,11 +9806,16 @@ var Orchestrator = class {
9285
9806
  }
9286
9807
  }
9287
9808
  }
9288
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
9289
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
9809
+ const lastStderr = summarizeTurnStderr(stderrTail);
9810
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9811
+ let chatText = assistantText;
9812
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
9813
+ chatText = humanizeAgentFailDetail(detail);
9814
+ }
9815
+ if (chatText || parts.length > 0) {
9290
9816
  appendMessage(threadId, {
9291
9817
  role: "agent",
9292
- text: assistantText,
9818
+ text: chatText,
9293
9819
  parts: parts.length > 0 ? parts : void 0,
9294
9820
  durationMs: Math.max(0, Date.now() - turnStartedAt),
9295
9821
  usage,
@@ -9308,13 +9834,12 @@ var Orchestrator = class {
9308
9834
  this.emit({ type: "status_changed", threadId, status: "stopped" });
9309
9835
  this.emit({ type: "turn_finished", threadId, exitCode });
9310
9836
  } else {
9311
- const lastStderr = summarizeTurnStderr(stderrTail);
9312
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9313
9837
  const failDetail = formatTurnExitError(exitCode, detail);
9838
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
9314
9839
  setStatus(
9315
9840
  threadId,
9316
9841
  exitCode === 0 ? "idle" : "error",
9317
- exitCode === 0 ? null : failDetail
9842
+ exitCode === 0 || explainedInChat ? null : failDetail
9318
9843
  );
9319
9844
  this.emit({
9320
9845
  type: "status_changed",
@@ -9322,6 +9847,10 @@ var Orchestrator = class {
9322
9847
  status: exitCode === 0 ? "idle" : "error"
9323
9848
  });
9324
9849
  this.emit({ type: "turn_finished", threadId, exitCode });
9850
+ if (exitCode !== 0) {
9851
+ const blob = [chatText, detail].filter(Boolean).join("\n");
9852
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
9853
+ }
9325
9854
  }
9326
9855
  } catch (err) {
9327
9856
  const message = err instanceof Error ? err.message : String(err);
@@ -9335,6 +9864,7 @@ var Orchestrator = class {
9335
9864
  this.emit({ type: "error", threadId, message });
9336
9865
  this.emit({ type: "status_changed", threadId, status: "error" });
9337
9866
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
9867
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
9338
9868
  }
9339
9869
  } finally {
9340
9870
  this.startingTurns.delete(threadId);
@@ -9795,6 +10325,15 @@ var Orchestrator = class {
9795
10325
  setAutonomy(threadRef, autonomy) {
9796
10326
  return this.setThreadOptions(threadRef, { autonomy });
9797
10327
  }
10328
+ /**
10329
+ * Open a Review chat tab on a worktree thread (same as the desktop Review button)
10330
+ * and send the merge-readiness prefill.
10331
+ */
10332
+ async requestReview(threadRef) {
10333
+ const { tab } = await requestReview(threadRef, (ref, prompt) => this.send(ref, prompt));
10334
+ this.emit({ type: "status_changed", threadId: tab.id, status: tab.status });
10335
+ return tab;
10336
+ }
9798
10337
  setThreadOptions(threadRef, patch) {
9799
10338
  const thread = this.requireThread(threadRef);
9800
10339
  const next = {};
@@ -9919,7 +10458,7 @@ var Orchestrator = class {
9919
10458
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
9920
10459
  return setStatus(thread.id, "idle");
9921
10460
  }
9922
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
10461
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
9923
10462
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
9924
10463
  const { execa: execa7 } = await import("execa");
9925
10464
  const slug = thread.worktreePath.split("/").pop();
@@ -10092,6 +10631,7 @@ async function listIssues(repoPath) {
10092
10631
 
10093
10632
  // src/mcp/server.ts
10094
10633
  init_global_workspace();
10634
+ init_list_models();
10095
10635
 
10096
10636
  // src/mcp/archive-guard.ts
10097
10637
  init_global_workspace();
@@ -10137,7 +10677,7 @@ async function startMcpServer() {
10137
10677
  async () => {
10138
10678
  const threads = orch.getThreads(true);
10139
10679
  const lines = threads.map((t) => {
10140
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path24.basename)(t.repoPath) || t.repoPath;
10680
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
10141
10681
  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}` : ""}`;
10142
10682
  });
10143
10683
  return {
@@ -10480,6 +11020,148 @@ async function startMcpServer() {
10480
11020
  };
10481
11021
  }
10482
11022
  );
11023
+ server.tool(
11024
+ "request_review",
11025
+ "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.",
11026
+ { ref: import_zod.z.string().describe("Worktree thread id/ref to review") },
11027
+ async ({ ref }) => {
11028
+ try {
11029
+ const tab = await orch.requestReview(ref);
11030
+ const from = orch.getThread(ref);
11031
+ return {
11032
+ content: [
11033
+ {
11034
+ type: "text",
11035
+ text: JSON.stringify({
11036
+ id: tab.id,
11037
+ title: tab.title,
11038
+ status: tab.status,
11039
+ fromThreadId: from?.id ?? ref,
11040
+ link: `sideboard://thread/${tab.id}`
11041
+ })
11042
+ }
11043
+ ]
11044
+ };
11045
+ } catch (err) {
11046
+ const message = err instanceof Error ? err.message : String(err);
11047
+ return { content: [{ type: "text", text: message }], isError: true };
11048
+ }
11049
+ }
11050
+ );
11051
+ const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
11052
+ server.tool(
11053
+ "list_models",
11054
+ "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.",
11055
+ {
11056
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
11057
+ },
11058
+ async ({ agent }) => {
11059
+ try {
11060
+ const catalogs = await listModelsForAgent(agent);
11061
+ return {
11062
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
11063
+ };
11064
+ } catch (err) {
11065
+ const message = err instanceof Error ? err.message : String(err);
11066
+ return { content: [{ type: "text", text: message }], isError: true };
11067
+ }
11068
+ }
11069
+ );
11070
+ server.tool(
11071
+ "fork_worktree",
11072
+ "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.",
11073
+ {
11074
+ ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
11075
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11076
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11077
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11078
+ title: import_zod.z.string().optional()
11079
+ },
11080
+ async ({ ref, through_index, agent, model, title }) => {
11081
+ try {
11082
+ const source = orch.getThread(ref);
11083
+ if (source) await orch.reconcile(source.repoPath);
11084
+ const thread = await orch.forkThreadWorktree({
11085
+ threadId: ref,
11086
+ throughIndex: through_index,
11087
+ agent,
11088
+ model,
11089
+ title
11090
+ });
11091
+ return {
11092
+ content: [
11093
+ {
11094
+ type: "text",
11095
+ text: JSON.stringify({
11096
+ id: thread.id,
11097
+ title: thread.title,
11098
+ status: thread.status,
11099
+ agent: thread.agent,
11100
+ model: thread.model,
11101
+ branchName: thread.branchName,
11102
+ worktreePath: thread.worktreePath,
11103
+ fromThreadId: source?.id ?? ref,
11104
+ link: `sideboard://thread/${thread.id}`
11105
+ })
11106
+ }
11107
+ ]
11108
+ };
11109
+ } catch (err) {
11110
+ const message = err instanceof Error ? err.message : String(err);
11111
+ return { content: [{ type: "text", text: message }], isError: true };
11112
+ }
11113
+ }
11114
+ );
11115
+ server.tool(
11116
+ "fork_chat",
11117
+ "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.",
11118
+ {
11119
+ ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
11120
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11121
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11122
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11123
+ title: import_zod.z.string().optional()
11124
+ },
11125
+ async ({ ref, through_index, agent, model, title }) => {
11126
+ try {
11127
+ const source = orch.getThread(ref);
11128
+ if (!source) {
11129
+ return {
11130
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
11131
+ isError: true
11132
+ };
11133
+ }
11134
+ const tab = orch.forkChatTab({
11135
+ threadId: source.id,
11136
+ throughIndex: through_index,
11137
+ agent,
11138
+ model,
11139
+ title
11140
+ });
11141
+ return {
11142
+ content: [
11143
+ {
11144
+ type: "text",
11145
+ text: JSON.stringify({
11146
+ id: tab.id,
11147
+ title: tab.title,
11148
+ status: tab.status,
11149
+ agent: tab.agent,
11150
+ model: tab.model,
11151
+ sourceType: tab.sourceType,
11152
+ worktreePath: tab.worktreePath,
11153
+ fromThreadId: source.id,
11154
+ link: `sideboard://thread/${tab.id}`
11155
+ })
11156
+ }
11157
+ ]
11158
+ };
11159
+ } catch (err) {
11160
+ const message = err instanceof Error ? err.message : String(err);
11161
+ return { content: [{ type: "text", text: message }], isError: true };
11162
+ }
11163
+ }
11164
+ );
10483
11165
  server.tool(
10484
11166
  "run_dev_script",
10485
11167
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",