@sideboard-ai/core 0.1.43 → 0.1.44

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.
@@ -3235,8 +3235,9 @@ var init_coordinator_prompt = __esm({
3235
3235
  "Setup / run:",
3236
3236
  "- run_setup \u2014 re-run worktree setup",
3237
3237
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
3238
- "Inspect / PRs:",
3238
+ "Inspect / review / PRs:",
3239
3239
  "- get_diff \u2014 compact diff summary",
3240
+ "- 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
3241
  "- 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
3242
  "Human-only (do not attempt): merge, ready-for-review land, purge_thread.",
3242
3243
  "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.",
@@ -5690,10 +5691,10 @@ __export(cursor_recover_exports, {
5690
5691
  function recoverFinishedCursorRun(opts) {
5691
5692
  const agentId = opts.agentId.trim();
5692
5693
  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;
5694
+ const runsPath = (0, import_node_path24.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
5695
+ if (!(0, import_node_fs25.existsSync)(runsPath)) return null;
5695
5696
  try {
5696
- const lines = (0, import_node_fs24.readFileSync)(runsPath, "utf8").split("\n");
5697
+ const lines = (0, import_node_fs25.readFileSync)(runsPath, "utf8").split("\n");
5697
5698
  let best = null;
5698
5699
  for (const line of lines) {
5699
5700
  const trimmed = line.trim();
@@ -5719,12 +5720,12 @@ function recoverFinishedCursorRun(opts) {
5719
5720
  return null;
5720
5721
  }
5721
5722
  }
5722
- var import_node_fs24, import_node_path23;
5723
+ var import_node_fs25, import_node_path24;
5723
5724
  var init_cursor_recover = __esm({
5724
5725
  "src/agents/cursor-recover.ts"() {
5725
5726
  "use strict";
5726
- import_node_fs24 = require("fs");
5727
- import_node_path23 = require("path");
5727
+ import_node_fs25 = require("fs");
5728
+ import_node_path24 = require("path");
5728
5729
  init_paths();
5729
5730
  }
5730
5731
  });
@@ -5733,11 +5734,11 @@ var init_cursor_recover = __esm({
5733
5734
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5734
5735
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5735
5736
  var import_zod = require("zod");
5736
- var import_node_path24 = require("path");
5737
+ var import_node_path25 = require("path");
5737
5738
 
5738
5739
  // src/orchestrator/orchestrator.ts
5739
5740
  var import_node_events = require("events");
5740
- var import_node_fs25 = require("fs");
5741
+ var import_node_fs26 = require("fs");
5741
5742
  init_error_detail();
5742
5743
 
5743
5744
  // src/agents/spawn.ts
@@ -7182,6 +7183,58 @@ function forkChatTab(input) {
7182
7183
  });
7183
7184
  }
7184
7185
 
7186
+ // src/review/request-review.ts
7187
+ var import_node_crypto3 = require("crypto");
7188
+ var import_node_fs19 = require("fs");
7189
+ var import_node_path18 = require("path");
7190
+ init_global_workspace();
7191
+ init_thread_store();
7192
+ var REVIEW_REQUEST_PATH = ".sideboard/attachments/Review request.md";
7193
+ var REVIEW_REQUEST_NAME = "Review request.md";
7194
+ var REVIEW_REQUEST_PREFILL = `Please review the changes in this workspace and recommend whether they are ready to merge.
7195
+
7196
+ 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).`;
7197
+ function buildReviewRequestAttachment(content) {
7198
+ return {
7199
+ id: (0, import_node_crypto3.randomUUID)(),
7200
+ name: REVIEW_REQUEST_NAME,
7201
+ kind: "file",
7202
+ path: REVIEW_REQUEST_PATH,
7203
+ content
7204
+ };
7205
+ }
7206
+ function readExistingReviewRequestFile(worktreePath) {
7207
+ const abs = (0, import_node_path18.join)(worktreePath, REVIEW_REQUEST_PATH);
7208
+ if (!(0, import_node_fs19.existsSync)(abs)) return null;
7209
+ try {
7210
+ const content = (0, import_node_fs19.readFileSync)(abs, "utf8");
7211
+ return content.trim() ? content : null;
7212
+ } catch {
7213
+ return null;
7214
+ }
7215
+ }
7216
+ async function requestReview(threadRef, send) {
7217
+ const from = findThreadByRef(threadRef);
7218
+ if (!from) throw new Error(`Thread not found: ${threadRef}`);
7219
+ if (isOrchestratorThread(from)) {
7220
+ throw new Error(
7221
+ "request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
7222
+ );
7223
+ }
7224
+ if (from.status === "archived") {
7225
+ throw new Error(`Thread is archived: ${from.id}`);
7226
+ }
7227
+ const existing = readExistingReviewRequestFile(from.worktreePath);
7228
+ const attachments = existing ? [buildReviewRequestAttachment(existing)] : [];
7229
+ const tab = createChatTab({
7230
+ fromThreadId: from.id,
7231
+ title: "Review",
7232
+ attachments
7233
+ });
7234
+ const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
7235
+ return { tab: started, from };
7236
+ }
7237
+
7185
7238
  // src/threads/fork-worktree.ts
7186
7239
  init_thread_store();
7187
7240
  function requireThread2(idOrRef) {
@@ -7215,20 +7268,20 @@ async function forkThreadWorktree(input, onSetupLine) {
7215
7268
 
7216
7269
  // src/threads/adopt.ts
7217
7270
  var import_node_child_process = require("child_process");
7218
- var import_node_fs19 = require("fs");
7271
+ var import_node_fs20 = require("fs");
7219
7272
  var import_node_os8 = require("os");
7220
- var import_node_path18 = require("path");
7273
+ var import_node_path19 = require("path");
7221
7274
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
7222
7275
  init_worktree();
7223
7276
  init_thread_store();
7224
- var CONDUCTOR_APP_SUPPORT = (0, import_node_path18.join)(
7277
+ var CONDUCTOR_APP_SUPPORT = (0, import_node_path19.join)(
7225
7278
  process.env.HOME ?? "",
7226
7279
  "Library",
7227
7280
  "Application Support",
7228
7281
  "com.conductor.app"
7229
7282
  );
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");
7283
+ var CONDUCTOR_DB = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
7284
+ var CURSOR_SDK_STORE = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
7232
7285
  function mapAgentType(raw) {
7233
7286
  if (!raw) return null;
7234
7287
  const v = raw.toLowerCase();
@@ -7240,21 +7293,21 @@ function mapAgentType(raw) {
7240
7293
  return null;
7241
7294
  }
7242
7295
  function resolveConductorCursorAgentId(workspacePath) {
7243
- if (!workspacePath || !(0, import_node_fs19.existsSync)(CURSOR_SDK_STORE)) return null;
7296
+ if (!workspacePath || !(0, import_node_fs20.existsSync)(CURSOR_SDK_STORE)) return null;
7244
7297
  const normalized = workspacePath.replace(/\/$/, "");
7245
7298
  let best = null;
7246
7299
  let hashes;
7247
7300
  try {
7248
- hashes = (0, import_node_fs19.readdirSync)(CURSOR_SDK_STORE);
7301
+ hashes = (0, import_node_fs20.readdirSync)(CURSOR_SDK_STORE);
7249
7302
  } catch {
7250
7303
  return null;
7251
7304
  }
7252
7305
  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;
7306
+ const agentsFile = (0, import_node_path19.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
7307
+ if (!(0, import_node_fs20.existsSync)(agentsFile)) continue;
7255
7308
  let text;
7256
7309
  try {
7257
- text = (0, import_node_fs19.readFileSync)(agentsFile, "utf8");
7310
+ text = (0, import_node_fs20.readFileSync)(agentsFile, "utf8");
7258
7311
  } catch {
7259
7312
  continue;
7260
7313
  }
@@ -7278,7 +7331,7 @@ function resolveConductorCursorAgentId(workspacePath) {
7278
7331
  return best?.agentId ?? null;
7279
7332
  }
7280
7333
  async function adoptThread(input) {
7281
- if (!(0, import_node_fs19.existsSync)(input.worktreePath)) {
7334
+ if (!(0, import_node_fs20.existsSync)(input.worktreePath)) {
7282
7335
  throw new Error(`Worktree not found: ${input.worktreePath}`);
7283
7336
  }
7284
7337
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -7302,18 +7355,18 @@ async function adoptThread(input) {
7302
7355
  return thread;
7303
7356
  }
7304
7357
  function listConductorWorkspaces() {
7305
- if (!(0, import_node_fs19.existsSync)(CONDUCTOR_DB)) {
7358
+ if (!(0, import_node_fs20.existsSync)(CONDUCTOR_DB)) {
7306
7359
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
7307
7360
  }
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");
7361
+ const tmp = (0, import_node_fs20.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7362
+ const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
7310
7363
  try {
7311
- (0, import_node_fs19.copyFileSync)(CONDUCTOR_DB, snapshot);
7364
+ (0, import_node_fs20.copyFileSync)(CONDUCTOR_DB, snapshot);
7312
7365
  for (const suffix of ["-wal", "-shm"]) {
7313
7366
  const src = `${CONDUCTOR_DB}${suffix}`;
7314
- if ((0, import_node_fs19.existsSync)(src)) {
7367
+ if ((0, import_node_fs20.existsSync)(src)) {
7315
7368
  try {
7316
- (0, import_node_fs19.copyFileSync)(src, `${snapshot}${suffix}`);
7369
+ (0, import_node_fs20.copyFileSync)(src, `${snapshot}${suffix}`);
7317
7370
  } catch {
7318
7371
  }
7319
7372
  }
@@ -7389,22 +7442,22 @@ function listConductorWorkspaces() {
7389
7442
  db.close();
7390
7443
  }
7391
7444
  } finally {
7392
- (0, import_node_fs19.rmSync)(tmp, { recursive: true, force: true });
7445
+ (0, import_node_fs20.rmSync)(tmp, { recursive: true, force: true });
7393
7446
  }
7394
7447
  }
7395
7448
  function importConductorWorkspace(workspaceId) {
7396
- if (!(0, import_node_fs19.existsSync)(CONDUCTOR_DB)) {
7449
+ if (!(0, import_node_fs20.existsSync)(CONDUCTOR_DB)) {
7397
7450
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
7398
7451
  }
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");
7452
+ const tmp = (0, import_node_fs20.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
7453
+ const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
7401
7454
  try {
7402
- (0, import_node_fs19.copyFileSync)(CONDUCTOR_DB, snapshot);
7455
+ (0, import_node_fs20.copyFileSync)(CONDUCTOR_DB, snapshot);
7403
7456
  for (const suffix of ["-wal", "-shm"]) {
7404
7457
  const src = `${CONDUCTOR_DB}${suffix}`;
7405
- if ((0, import_node_fs19.existsSync)(src)) {
7458
+ if ((0, import_node_fs20.existsSync)(src)) {
7406
7459
  try {
7407
- (0, import_node_fs19.copyFileSync)(src, `${snapshot}${suffix}`);
7460
+ (0, import_node_fs20.copyFileSync)(src, `${snapshot}${suffix}`);
7408
7461
  } catch {
7409
7462
  }
7410
7463
  }
@@ -7422,7 +7475,7 @@ function importConductorWorkspace(workspaceId) {
7422
7475
  ).get(workspaceId);
7423
7476
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
7424
7477
  const worktreePath = String(row.workspacePath);
7425
- if (!(0, import_node_fs19.existsSync)(worktreePath)) {
7478
+ if (!(0, import_node_fs20.existsSync)(worktreePath)) {
7426
7479
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
7427
7480
  }
7428
7481
  let sessionId = null;
@@ -7485,7 +7538,7 @@ function importConductorWorkspace(workspaceId) {
7485
7538
  db.close();
7486
7539
  }
7487
7540
  } finally {
7488
- (0, import_node_fs19.rmSync)(tmp, { recursive: true, force: true });
7541
+ (0, import_node_fs20.rmSync)(tmp, { recursive: true, force: true });
7489
7542
  }
7490
7543
  }
7491
7544
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -7496,12 +7549,12 @@ async function importConductorWorkspaceAsync(workspaceId) {
7496
7549
  init_worktree();
7497
7550
 
7498
7551
  // src/diff/diff.ts
7499
- var import_node_fs20 = require("fs");
7500
- var import_node_path19 = require("path");
7552
+ var import_node_fs21 = require("fs");
7553
+ var import_node_path20 = require("path");
7501
7554
  init_run();
7502
7555
  init_worktree();
7503
7556
  async function inspectGitWorktree(worktreePath) {
7504
- if (!worktreePath || !(0, import_node_fs20.existsSync)(worktreePath)) return "missing_worktree";
7557
+ if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) return "missing_worktree";
7505
7558
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
7506
7559
  reject: false
7507
7560
  });
@@ -7509,7 +7562,7 @@ async function inspectGitWorktree(worktreePath) {
7509
7562
  return "ok";
7510
7563
  }
7511
7564
  async function initializeGitRepository(worktreePath) {
7512
- if (!worktreePath || !(0, import_node_fs20.existsSync)(worktreePath)) {
7565
+ if (!worktreePath || !(0, import_node_fs21.existsSync)(worktreePath)) {
7513
7566
  throw new Error("Worktree not found");
7514
7567
  }
7515
7568
  const status = await inspectGitWorktree(worktreePath);
@@ -7941,8 +7994,8 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
7941
7994
  function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7942
7995
  assertSafeRelativePath(relativePath);
7943
7996
  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);
7997
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
7998
+ const st = (0, import_node_fs21.statSync)(abs);
7946
7999
  if (!st.isFile()) {
7947
8000
  throw new Error(`Not a file: ${relativePath}`);
7948
8001
  }
@@ -7951,7 +8004,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7951
8004
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
7952
8005
  );
7953
8006
  }
7954
- const buf = (0, import_node_fs20.readFileSync)(abs);
8007
+ const buf = (0, import_node_fs21.readFileSync)(abs);
7955
8008
  return {
7956
8009
  path: relativePath,
7957
8010
  contentBase64: buf.toString("base64"),
@@ -7961,12 +8014,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
7961
8014
  function readWorktreeFile(worktreePath, relativePath, opts) {
7962
8015
  assertSafeRelativePath(relativePath);
7963
8016
  const maxBytes = opts?.maxBytes ?? 2e5;
7964
- const abs = (0, import_node_path19.join)(worktreePath, relativePath);
7965
- const st = (0, import_node_fs20.statSync)(abs);
8017
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8018
+ const st = (0, import_node_fs21.statSync)(abs);
7966
8019
  if (!st.isFile()) {
7967
8020
  throw new Error(`Not a file: ${relativePath}`);
7968
8021
  }
7969
- const buf = (0, import_node_fs20.readFileSync)(abs);
8022
+ const buf = (0, import_node_fs21.readFileSync)(abs);
7970
8023
  if (isImageRelativePath(relativePath)) {
7971
8024
  const maxImageBytes = Math.max(maxBytes, 15e6);
7972
8025
  const truncated2 = buf.length > maxImageBytes;
@@ -8009,9 +8062,9 @@ function assertSafeRelativePath(relativePath) {
8009
8062
  }
8010
8063
  function writeWorktreeFile(worktreePath, relativePath, content) {
8011
8064
  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");
8065
+ const abs = (0, import_node_path20.join)(worktreePath, relativePath);
8066
+ (0, import_node_fs21.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
8067
+ (0, import_node_fs21.writeFileSync)(abs, content, "utf8");
8015
8068
  return { path: relativePath };
8016
8069
  }
8017
8070
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -8190,9 +8243,9 @@ async function confirmLand(thread, opts) {
8190
8243
  }
8191
8244
 
8192
8245
  // src/skills/discover.ts
8193
- var import_node_fs21 = require("fs");
8246
+ var import_node_fs22 = require("fs");
8194
8247
  var import_node_os9 = require("os");
8195
- var import_node_path20 = require("path");
8248
+ var import_node_path21 = require("path");
8196
8249
  function toCommand(name) {
8197
8250
  return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8198
8251
  }
@@ -8224,7 +8277,7 @@ function parseFrontmatter(content) {
8224
8277
  }
8225
8278
  function readSkill(skillMd, source) {
8226
8279
  try {
8227
- const content = (0, import_node_fs21.readFileSync)(skillMd, "utf8");
8280
+ const content = (0, import_node_fs22.readFileSync)(skillMd, "utf8");
8228
8281
  const { name: fmName, description } = parseFrontmatter(content);
8229
8282
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
8230
8283
  const name = fmName || dirName;
@@ -8243,19 +8296,19 @@ function readSkill(skillMd, source) {
8243
8296
  }
8244
8297
  }
8245
8298
  function scanSkillsDir(dir, source, out) {
8246
- if (!(0, import_node_fs21.existsSync)(dir)) return;
8299
+ if (!(0, import_node_fs22.existsSync)(dir)) return;
8247
8300
  let entries;
8248
8301
  try {
8249
- entries = (0, import_node_fs21.readdirSync)(dir);
8302
+ entries = (0, import_node_fs22.readdirSync)(dir);
8250
8303
  } catch {
8251
8304
  return;
8252
8305
  }
8253
8306
  for (const entry of entries) {
8254
8307
  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;
8308
+ const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
8309
+ if (!(0, import_node_fs22.existsSync)(skillMd)) continue;
8257
8310
  try {
8258
- if (!(0, import_node_fs21.statSync)(skillMd).isFile()) continue;
8311
+ if (!(0, import_node_fs22.statSync)(skillMd).isFile()) continue;
8259
8312
  } catch {
8260
8313
  continue;
8261
8314
  }
@@ -8264,24 +8317,24 @@ function scanSkillsDir(dir, source, out) {
8264
8317
  }
8265
8318
  }
8266
8319
  function scanClaudePluginSkills(pluginsRoot, out) {
8267
- if (!(0, import_node_fs21.existsSync)(pluginsRoot)) return;
8320
+ if (!(0, import_node_fs22.existsSync)(pluginsRoot)) return;
8268
8321
  const walk = (dir, depth, lookingForSkillsDir) => {
8269
8322
  if (depth > 7) return;
8270
8323
  let entries;
8271
8324
  try {
8272
- entries = (0, import_node_fs21.readdirSync)(dir);
8325
+ entries = (0, import_node_fs22.readdirSync)(dir);
8273
8326
  } catch {
8274
8327
  return;
8275
8328
  }
8276
8329
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
8277
- const skill = readSkill((0, import_node_path20.join)(dir, "SKILL.md"), "cli");
8330
+ const skill = readSkill((0, import_node_path21.join)(dir, "SKILL.md"), "cli");
8278
8331
  if (skill) out.push(skill);
8279
8332
  }
8280
8333
  for (const entry of entries) {
8281
8334
  if (entry === "node_modules" || entry === ".git") continue;
8282
- const full = (0, import_node_path20.join)(dir, entry);
8335
+ const full = (0, import_node_path21.join)(dir, entry);
8283
8336
  try {
8284
- if (!(0, import_node_fs21.statSync)(full).isDirectory()) continue;
8337
+ if (!(0, import_node_fs22.statSync)(full).isDirectory()) continue;
8285
8338
  } catch {
8286
8339
  continue;
8287
8340
  }
@@ -8299,17 +8352,17 @@ function discoverSkills(worktreePath) {
8299
8352
  const home = (0, import_node_os9.homedir)();
8300
8353
  const collected = [];
8301
8354
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
8302
- scanSkillsDir((0, import_node_path20.join)(worktreePath, rel), "workspace", collected);
8355
+ scanSkillsDir((0, import_node_path21.join)(worktreePath, rel), "workspace", collected);
8303
8356
  }
8304
8357
  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")
8358
+ (0, import_node_path21.join)(home, ".claude/skills"),
8359
+ (0, import_node_path21.join)(home, ".cursor/skills"),
8360
+ (0, import_node_path21.join)(home, ".sideboard/skills"),
8361
+ (0, import_node_path21.join)(home, ".brightsy/skills")
8309
8362
  ]) {
8310
8363
  scanSkillsDir(abs, "user", collected);
8311
8364
  }
8312
- scanClaudePluginSkills((0, import_node_path20.join)(home, ".claude/plugins"), collected);
8365
+ scanClaudePluginSkills((0, import_node_path21.join)(home, ".claude/plugins"), collected);
8313
8366
  const rank = { workspace: 0, user: 1, cli: 2 };
8314
8367
  const byCommand = /* @__PURE__ */ new Map();
8315
8368
  for (const skill of collected) {
@@ -8321,7 +8374,7 @@ function discoverSkills(worktreePath) {
8321
8374
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
8322
8375
  }
8323
8376
  function readSkillBody(skillPath, maxChars = 12e3) {
8324
- const raw = (0, import_node_fs21.readFileSync)(skillPath, "utf8");
8377
+ const raw = (0, import_node_fs22.readFileSync)(skillPath, "utf8");
8325
8378
  if (raw.startsWith("---")) {
8326
8379
  const end = raw.indexOf("\n---", 3);
8327
8380
  if (end >= 0) {
@@ -8414,9 +8467,9 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
8414
8467
  }
8415
8468
 
8416
8469
  // 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");
8470
+ var import_node_fs23 = require("fs");
8471
+ var import_node_path22 = require("path");
8472
+ var import_node_crypto4 = require("crypto");
8420
8473
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
8421
8474
  "png",
8422
8475
  "jpg",
@@ -8445,7 +8498,7 @@ var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local on
8445
8498
  var MAX_INLINE_BYTES = 4e5;
8446
8499
  var MAX_PREVIEW_BYTES = 5e6;
8447
8500
  function fileExtension(filePath) {
8448
- const base = (0, import_node_path21.basename)(filePath).toLowerCase();
8501
+ const base = (0, import_node_path22.basename)(filePath).toLowerCase();
8449
8502
  return base.includes(".") ? base.split(".").pop() || "" : "";
8450
8503
  }
8451
8504
  function isImageFilePath(filePath) {
@@ -8455,24 +8508,24 @@ function imageMimeType(filePath) {
8455
8508
  return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
8456
8509
  }
8457
8510
  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");
8511
+ const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
8512
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
8513
+ const gi = (0, import_node_path22.join)(dir, ".gitignore");
8514
+ if (!(0, import_node_fs23.existsSync)(gi)) {
8515
+ (0, import_node_fs23.writeFileSync)(gi, ATTACHMENTS_GITIGNORE, "utf8");
8463
8516
  }
8464
8517
  return dir;
8465
8518
  }
8466
8519
  function uniqueAttachmentName(dir, originalName) {
8467
8520
  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);
8521
+ if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
8522
+ const ext = (0, import_node_path22.extname)(safe);
8470
8523
  const stem = ext ? safe.slice(0, -ext.length) : safe;
8471
8524
  for (let i = 1; i < 1e4; i++) {
8472
8525
  const candidate = `${stem}-${i}${ext}`;
8473
- if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, candidate))) return candidate;
8526
+ if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
8474
8527
  }
8475
- return `${stem}-${(0, import_node_crypto3.randomUUID)()}${ext}`;
8528
+ return `${stem}-${(0, import_node_crypto4.randomUUID)()}${ext}`;
8476
8529
  }
8477
8530
  function previewDataUrlFromBuf(filePath, buf) {
8478
8531
  if (!isImageFilePath(filePath)) return void 0;
@@ -8484,7 +8537,7 @@ function attachmentFromBuffer(name, buf, opts) {
8484
8537
  if (isImageFilePath(name)) {
8485
8538
  const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
8486
8539
  return {
8487
- id: (0, import_node_crypto3.randomUUID)(),
8540
+ id: (0, import_node_crypto4.randomUUID)(),
8488
8541
  name,
8489
8542
  kind: "file",
8490
8543
  path: opts.path,
@@ -8497,7 +8550,7 @@ function attachmentFromBuffer(name, buf, opts) {
8497
8550
  }
8498
8551
  if (buf.length > MAX_INLINE_BYTES) {
8499
8552
  return {
8500
- id: (0, import_node_crypto3.randomUUID)(),
8553
+ id: (0, import_node_crypto4.randomUUID)(),
8501
8554
  name,
8502
8555
  kind: "file",
8503
8556
  path: opts.path,
@@ -8506,7 +8559,7 @@ function attachmentFromBuffer(name, buf, opts) {
8506
8559
  }
8507
8560
  if (buf.includes(0)) {
8508
8561
  return {
8509
- id: (0, import_node_crypto3.randomUUID)(),
8562
+ id: (0, import_node_crypto4.randomUUID)(),
8510
8563
  name,
8511
8564
  kind: "file",
8512
8565
  path: opts.path,
@@ -8514,7 +8567,7 @@ function attachmentFromBuffer(name, buf, opts) {
8514
8567
  };
8515
8568
  }
8516
8569
  return {
8517
- id: (0, import_node_crypto3.randomUUID)(),
8570
+ id: (0, import_node_crypto4.randomUUID)(),
8518
8571
  name,
8519
8572
  kind: "file",
8520
8573
  path: opts.path,
@@ -8526,19 +8579,19 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
8526
8579
  const dir = ensureAttachmentsDir(worktreePath);
8527
8580
  const out = [];
8528
8581
  for (const abs of absolutePaths) {
8529
- const originalName = (0, import_node_path21.basename)(abs);
8582
+ const originalName = (0, import_node_path22.basename)(abs);
8530
8583
  try {
8531
- const st = (0, import_node_fs22.statSync)(abs);
8584
+ const st = (0, import_node_fs23.statSync)(abs);
8532
8585
  if (!st.isFile()) continue;
8533
8586
  const name = uniqueAttachmentName(dir, originalName);
8534
- const destAbs = (0, import_node_path21.join)(dir, name);
8535
- (0, import_node_fs22.copyFileSync)(abs, destAbs);
8587
+ const destAbs = (0, import_node_path22.join)(dir, name);
8588
+ (0, import_node_fs23.copyFileSync)(abs, destAbs);
8536
8589
  const rel = `${ATTACHMENTS_DIR}/${name}`;
8537
- const buf = (0, import_node_fs22.readFileSync)(destAbs);
8590
+ const buf = (0, import_node_fs23.readFileSync)(destAbs);
8538
8591
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8539
8592
  } catch (err) {
8540
8593
  out.push({
8541
- id: (0, import_node_crypto3.randomUUID)(),
8594
+ id: (0, import_node_crypto4.randomUUID)(),
8542
8595
  name: originalName,
8543
8596
  kind: "file",
8544
8597
  content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
@@ -8556,13 +8609,13 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
8556
8609
  try {
8557
8610
  const buf = Buffer.from(item.dataBase64, "base64");
8558
8611
  const name = uniqueAttachmentName(dir, originalName);
8559
- const destAbs = (0, import_node_path21.join)(dir, name);
8560
- (0, import_node_fs22.writeFileSync)(destAbs, buf);
8612
+ const destAbs = (0, import_node_path22.join)(dir, name);
8613
+ (0, import_node_fs23.writeFileSync)(destAbs, buf);
8561
8614
  const rel = `${ATTACHMENTS_DIR}/${name}`;
8562
8615
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
8563
8616
  } catch (err) {
8564
8617
  out.push({
8565
- id: (0, import_node_crypto3.randomUUID)(),
8618
+ id: (0, import_node_crypto4.randomUUID)(),
8566
8619
  name: originalName,
8567
8620
  kind: "file",
8568
8621
  content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
@@ -8576,23 +8629,23 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8576
8629
  for (const rel of relativePaths) {
8577
8630
  if (!rel || rel.includes("..") || rel.startsWith("/")) {
8578
8631
  out.push({
8579
- id: (0, import_node_crypto3.randomUUID)(),
8580
- name: (0, import_node_path21.basename)(rel) || "file",
8632
+ id: (0, import_node_crypto4.randomUUID)(),
8633
+ name: (0, import_node_path22.basename)(rel) || "file",
8581
8634
  kind: "file",
8582
8635
  content: `(invalid path: ${rel})`
8583
8636
  });
8584
8637
  continue;
8585
8638
  }
8586
- const name = (0, import_node_path21.basename)(rel);
8639
+ const name = (0, import_node_path22.basename)(rel);
8587
8640
  try {
8588
- const abs = (0, import_node_path21.join)(worktreePath, rel);
8589
- const st = (0, import_node_fs22.statSync)(abs);
8641
+ const abs = (0, import_node_path22.join)(worktreePath, rel);
8642
+ const st = (0, import_node_fs23.statSync)(abs);
8590
8643
  if (!st.isFile()) continue;
8591
- const buf = (0, import_node_fs22.readFileSync)(abs);
8644
+ const buf = (0, import_node_fs23.readFileSync)(abs);
8592
8645
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8593
8646
  } catch (err) {
8594
8647
  out.push({
8595
- id: (0, import_node_crypto3.randomUUID)(),
8648
+ id: (0, import_node_crypto4.randomUUID)(),
8596
8649
  name,
8597
8650
  kind: "file",
8598
8651
  content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
@@ -8603,8 +8656,8 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8603
8656
  }
8604
8657
 
8605
8658
  // src/agents/instructions.ts
8606
- var import_node_fs23 = require("fs");
8607
- var import_node_path22 = require("path");
8659
+ var import_node_fs24 = require("fs");
8660
+ var import_node_path23 = require("path");
8608
8661
  init_worktree_labels();
8609
8662
  function normPath2(p) {
8610
8663
  return p.replace(/\/+$/, "");
@@ -8743,11 +8796,11 @@ function loadAgentInstructions(worktreePath, agent) {
8743
8796
  const out = [];
8744
8797
  for (const rel of candidates) {
8745
8798
  if (seen.has(rel)) continue;
8746
- const abs = (0, import_node_path22.join)(worktreePath, rel);
8747
- if (!(0, import_node_fs23.existsSync)(abs)) continue;
8799
+ const abs = (0, import_node_path23.join)(worktreePath, rel);
8800
+ if (!(0, import_node_fs24.existsSync)(abs)) continue;
8748
8801
  try {
8749
- if (!(0, import_node_fs23.statSync)(abs).isFile()) continue;
8750
- let content = (0, import_node_fs23.readFileSync)(abs, "utf8");
8802
+ if (!(0, import_node_fs24.statSync)(abs).isFile()) continue;
8803
+ let content = (0, import_node_fs24.readFileSync)(abs, "utf8");
8751
8804
  if (!content.trim()) continue;
8752
8805
  if (content.length > MAX_CHARS_PER_FILE) {
8753
8806
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -8897,7 +8950,7 @@ var Orchestrator = class {
8897
8950
  }
8898
8951
  continue;
8899
8952
  }
8900
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
8953
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
8901
8954
  setStatus(thread.id, "broken", "Worktree missing on disk");
8902
8955
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
8903
8956
  continue;
@@ -9795,6 +9848,15 @@ var Orchestrator = class {
9795
9848
  setAutonomy(threadRef, autonomy) {
9796
9849
  return this.setThreadOptions(threadRef, { autonomy });
9797
9850
  }
9851
+ /**
9852
+ * Open a Review chat tab on a worktree thread (same as the desktop Review button)
9853
+ * and send the merge-readiness prefill.
9854
+ */
9855
+ async requestReview(threadRef) {
9856
+ const { tab } = await requestReview(threadRef, (ref, prompt) => this.send(ref, prompt));
9857
+ this.emit({ type: "status_changed", threadId: tab.id, status: tab.status });
9858
+ return tab;
9859
+ }
9798
9860
  setThreadOptions(threadRef, patch) {
9799
9861
  const thread = this.requireThread(threadRef);
9800
9862
  const next = {};
@@ -9919,7 +9981,7 @@ var Orchestrator = class {
9919
9981
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
9920
9982
  return setStatus(thread.id, "idle");
9921
9983
  }
9922
- if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
9984
+ if (!(0, import_node_fs26.existsSync)(thread.worktreePath)) {
9923
9985
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
9924
9986
  const { execa: execa7 } = await import("execa");
9925
9987
  const slug = thread.worktreePath.split("/").pop();
@@ -10137,7 +10199,7 @@ async function startMcpServer() {
10137
10199
  async () => {
10138
10200
  const threads = orch.getThreads(true);
10139
10201
  const lines = threads.map((t) => {
10140
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path24.basename)(t.repoPath) || t.repoPath;
10202
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path25.basename)(t.repoPath) || t.repoPath;
10141
10203
  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
10204
  });
10143
10205
  return {
@@ -10480,6 +10542,34 @@ async function startMcpServer() {
10480
10542
  };
10481
10543
  }
10482
10544
  );
10545
+ server.tool(
10546
+ "request_review",
10547
+ "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.",
10548
+ { ref: import_zod.z.string().describe("Worktree thread id/ref to review") },
10549
+ async ({ ref }) => {
10550
+ try {
10551
+ const tab = await orch.requestReview(ref);
10552
+ const from = orch.getThread(ref);
10553
+ return {
10554
+ content: [
10555
+ {
10556
+ type: "text",
10557
+ text: JSON.stringify({
10558
+ id: tab.id,
10559
+ title: tab.title,
10560
+ status: tab.status,
10561
+ fromThreadId: from?.id ?? ref,
10562
+ link: `sideboard://thread/${tab.id}`
10563
+ })
10564
+ }
10565
+ ]
10566
+ };
10567
+ } catch (err) {
10568
+ const message = err instanceof Error ? err.message : String(err);
10569
+ return { content: [{ type: "text", text: message }], isError: true };
10570
+ }
10571
+ }
10572
+ );
10483
10573
  server.tool(
10484
10574
  "run_dev_script",
10485
10575
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",