@sideboard-ai/core 0.1.61 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{agents-B2723EOF.js → agents-EWFD765A.js} +23 -7
  2. package/dist/{agents-EADRAAPG.js → agents-MD7SCFYX.js} +23 -7
  3. package/dist/{app-settings-YBNZ4XSR.js → app-settings-GJVZGU3M.js} +15 -3
  4. package/dist/{app-settings-TBHTJWLQ.js → app-settings-ONKMFSEJ.js} +15 -3
  5. package/dist/{chunk-RIWUU7FO.js → chunk-5AG5BUQN.js} +61 -3
  6. package/dist/{chunk-HLEX5AQ6.js → chunk-7FD7COKE.js} +4 -0
  7. package/dist/{chunk-XWPKLRB3.js → chunk-AKPEXIKK.js} +2 -2
  8. package/dist/{chunk-MX7UH5AT.js → chunk-BBEH2CJE.js} +264 -93
  9. package/dist/{chunk-FGNBTFRD.js → chunk-BPOTVI5J.js} +125 -3
  10. package/dist/{chunk-CHGOMFTA.js → chunk-DNPP7QMK.js} +32 -7
  11. package/dist/{chunk-MBAL5FE6.js → chunk-DZYMNW2A.js} +4 -4
  12. package/dist/{chunk-JB2H7E7V.js → chunk-EVD4KNKR.js} +1 -1
  13. package/dist/{chunk-TSRXOSVD.js → chunk-FWJYLBO6.js} +4 -0
  14. package/dist/{chunk-QJBC4BRK.js → chunk-HTOTTJJX.js} +2 -2
  15. package/dist/{chunk-MFGGIRUA.js → chunk-I2KGRAXC.js} +2 -2
  16. package/dist/{chunk-ZXDOI6IH.js → chunk-IFAU2DEY.js} +269 -93
  17. package/dist/{chunk-NTYRBRE7.js → chunk-LRBRQFBU.js} +2 -2
  18. package/dist/{chunk-75QR74KM.js → chunk-MHR4OGZ4.js} +125 -3
  19. package/dist/{chunk-2P2UK7E7.js → chunk-PESJDZTB.js} +2 -2
  20. package/dist/{chunk-P7EQUFB6.js → chunk-R7GUDEPY.js} +2 -2
  21. package/dist/{chunk-73AT5K4A.js → chunk-TXQ6K2GR.js} +61 -3
  22. package/dist/{chunk-MLAST5BB.js → chunk-YTNLOJPK.js} +32 -7
  23. package/dist/{connected-teams-RSL4VB3X.js → connected-teams-CHXO5FFA.js} +2 -2
  24. package/dist/{connected-teams-MKIYKOYA.js → connected-teams-GJTESSTX.js} +2 -2
  25. package/dist/{coordinator-prompt-4P23VVFC.js → coordinator-prompt-E5UGSJES.js} +5 -5
  26. package/dist/{coordinator-prompt-LNWHZ64D.js → coordinator-prompt-ILSULI7C.js} +5 -5
  27. package/dist/{global-workspace-TMMAVRVW.js → global-workspace-44UG64NA.js} +6 -7
  28. package/dist/{global-workspace-BHT4XUAA.js → global-workspace-5PM5LA6E.js} +6 -7
  29. package/dist/index.cjs +1433 -944
  30. package/dist/index.d.cts +117 -2
  31. package/dist/index.d.ts +117 -2
  32. package/dist/index.js +859 -736
  33. package/dist/mcp/run-stdio.cjs +1388 -993
  34. package/dist/mcp/run-stdio.js +798 -765
  35. package/dist/{run-HZTBWGQR.js → run-74S2NZWB.js} +1 -1
  36. package/dist/{run-GPSVV5OH.js → run-JLN5JK7Q.js} +1 -1
  37. package/dist/{thread-store-GHOADGL2.js → thread-store-BUBR24YA.js} +1 -1
  38. package/dist/{thread-store-UJIGMI5J.js → thread-store-XTPFCJAI.js} +1 -1
  39. package/dist/{workspaces-3GO3EDQG.js → workspaces-OVBCSS5Y.js} +7 -8
  40. package/dist/{workspaces-GFPGDGCS.js → workspaces-RXFXQRRF.js} +7 -8
  41. package/dist/{worktree-GBLHEFHR.js → worktree-K2JB53R5.js} +3 -3
  42. package/dist/{worktree-3L2G6PCM.js → worktree-S2N63FLV.js} +3 -3
  43. package/package.json +1 -1
  44. package/dist/chunk-GI7E5733.js +0 -30
  45. package/dist/chunk-S2LL5HA4.js +0 -33
@@ -14,15 +14,16 @@ import {
14
14
  parseSessionQuotaResetAt,
15
15
  pushTurnStderr,
16
16
  resolveQuotaFallbackAgent,
17
+ sideboardMcpProfile,
17
18
  summarizeTurnStderr
18
- } from "../chunk-MX7UH5AT.js";
19
- import "../chunk-JB2H7E7V.js";
19
+ } from "../chunk-BBEH2CJE.js";
20
+ import "../chunk-EVD4KNKR.js";
20
21
  import {
21
22
  addWorkspace,
22
23
  ensureWorkspace,
23
24
  removeWorkspace,
24
25
  syncWorkspacesFromThreads
25
- } from "../chunk-QJBC4BRK.js";
26
+ } from "../chunk-HTOTTJJX.js";
26
27
  import {
27
28
  extractPresentedPlan,
28
29
  readPlanFile,
@@ -30,22 +31,20 @@ import {
30
31
  } from "../chunk-N5PM7HGQ.js";
31
32
  import {
32
33
  GLOBAL_WORKSPACE_ID,
34
+ assertOrchestratorCapableAgent,
33
35
  healOrchestrationSoccerTitles,
34
36
  isCloudCoordinatorThread,
35
37
  isGlobalRepoPath,
36
38
  isGlobalThread,
37
39
  isOrchestratorThread,
38
40
  orchestratorSessionPoisonedByBuiltins
39
- } from "../chunk-CHGOMFTA.js";
40
- import {
41
- assertOrchestratorCapableAgent
42
- } from "../chunk-S2LL5HA4.js";
41
+ } from "../chunk-DNPP7QMK.js";
43
42
  import {
44
43
  coordinatorSystemPrompt,
45
44
  coordinatorTurnReminder,
46
45
  enrichWorkspacesWithGithub,
47
46
  ensureGlobalCoordinatorCwd
48
- } from "../chunk-NTYRBRE7.js";
47
+ } from "../chunk-LRBRQFBU.js";
49
48
  import {
50
49
  addPrStackLayer,
51
50
  allocateTeamName,
@@ -82,7 +81,7 @@ import {
82
81
  takenSlugsFromThread,
83
82
  threadDisplayLabel,
84
83
  worktreeNameFromPath
85
- } from "../chunk-MFGGIRUA.js";
84
+ } from "../chunk-I2KGRAXC.js";
86
85
  import {
87
86
  ATTACHMENTS_DIR,
88
87
  LEGACY_ATTACHMENTS_DIR,
@@ -99,13 +98,14 @@ import {
99
98
  updateThread,
100
99
  withThreadLock,
101
100
  writeThread
102
- } from "../chunk-HLEX5AQ6.js";
101
+ } from "../chunk-7FD7COKE.js";
103
102
  import {
103
+ enrichPathWithNpmGlobalBin,
104
104
  ensureAgentPath,
105
105
  gh,
106
106
  git,
107
107
  run
108
- } from "../chunk-RIWUU7FO.js";
108
+ } from "../chunk-5AG5BUQN.js";
109
109
  import {
110
110
  childEnvWithAppSettings,
111
111
  getLinearApiKey,
@@ -116,7 +116,7 @@ import {
116
116
  resolveEffectiveIssueSource,
117
117
  resolveNewThreadOptions,
118
118
  updateAdvancedSettings
119
- } from "../chunk-FGNBTFRD.js";
119
+ } from "../chunk-BPOTVI5J.js";
120
120
  import "../chunk-KGZBYWZ3.js";
121
121
  import {
122
122
  loadRepoSettings,
@@ -391,9 +391,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
391
391
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
392
392
  );
393
393
  }
394
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-BHT4XUAA.js");
394
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-5PM5LA6E.js");
395
395
  if (isGlobalThread2(thread)) {
396
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-4P23VVFC.js");
396
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-E5UGSJES.js");
397
397
  ensureGlobalCoordinatorCwd2(
398
398
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
399
399
  );
@@ -495,6 +495,20 @@ async function spawnAgentTurn(thread, input, onEvent) {
495
495
  };
496
496
  }
497
497
 
498
+ // src/git/pr-merge-archive.ts
499
+ function normalizePrState(state) {
500
+ return (state ?? "").trim().toUpperCase();
501
+ }
502
+ function shouldAutoArchiveOnPrMerge(opts) {
503
+ if (!opts.autoArchiveEnabled) return false;
504
+ if (opts.isGlobal) return false;
505
+ if (opts.skipAutoArchiveOnMerge) return false;
506
+ if (opts.threadStatus === "archived") return false;
507
+ const next = normalizePrState(opts.nextPrState);
508
+ if (next !== "MERGED") return false;
509
+ return normalizePrState(opts.previousPrState) !== "MERGED";
510
+ }
511
+
498
512
  // src/hook/conductor.ts
499
513
  import {
500
514
  copyFileSync,
@@ -1103,6 +1117,7 @@ import { existsSync as existsSync5 } from "fs";
1103
1117
  var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
1104
1118
  async function requireAgent(agent, opts) {
1105
1119
  ensureAgentPath();
1120
+ enrichPathWithNpmGlobalBin();
1106
1121
  const status = await Promise.race([
1107
1122
  getAdapter(agent).detect(),
1108
1123
  new Promise((_, reject) => {
@@ -1194,7 +1209,7 @@ async function createThread(input, _onSetupLine) {
1194
1209
  return readThread(thread.id) ?? thread;
1195
1210
  }
1196
1211
  async function listLinearIssues(agent, repoPath) {
1197
- const { getAdapter: getAdapter2 } = await import("../agents-EADRAAPG.js");
1212
+ const { getAdapter: getAdapter2 } = await import("../agents-MD7SCFYX.js");
1198
1213
  await requireAgent(agent, { requireLinear: true });
1199
1214
  const adapter = getAdapter2(agent);
1200
1215
  if (!adapter.listLinearIssues) {
@@ -1512,6 +1527,8 @@ function worktreeBindingFrom(from) {
1512
1527
  parentThreadId: from.parentThreadId,
1513
1528
  prUrl: from.prUrl,
1514
1529
  prTitle: from.prTitle,
1530
+ prState: from.prState,
1531
+ skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
1515
1532
  stackId: from.stackId,
1516
1533
  stackLayer: from.stackLayer
1517
1534
  };
@@ -2041,7 +2058,7 @@ async function adoptThread(input) {
2041
2058
  throw new Error(`Worktree not found: ${input.worktreePath}`);
2042
2059
  }
2043
2060
  const repoPath = await resolveRepoRoot(input.worktreePath);
2044
- const { stdout: branch } = await import("../run-HZTBWGQR.js").then(
2061
+ const { stdout: branch } = await import("../run-74S2NZWB.js").then(
2045
2062
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], input.worktreePath)
2046
2063
  );
2047
2064
  const thread = createEmptyThread({
@@ -2056,7 +2073,7 @@ async function adoptThread(input) {
2056
2073
  messages: input.messages ?? []
2057
2074
  });
2058
2075
  writeThread(thread);
2059
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GFPGDGCS.js");
2076
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-RXFXQRRF.js");
2060
2077
  await ensureWorkspace2(repoPath);
2061
2078
  return thread;
2062
2079
  }
@@ -3162,7 +3179,7 @@ async function previewLand(thread) {
3162
3179
  const target = await resolveDefaultBranch(thread.repoPath);
3163
3180
  if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
3164
3181
  }
3165
- const { stdout: head } = await import("../run-HZTBWGQR.js").then(
3182
+ const { stdout: head } = await import("../run-74S2NZWB.js").then(
3166
3183
  ({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
3167
3184
  );
3168
3185
  const current = head.trim();
@@ -3201,7 +3218,7 @@ async function confirmLand(thread, opts) {
3201
3218
  if (preview.dirty) {
3202
3219
  committed = await commitAll(thread.worktreePath, meta.commitMessage);
3203
3220
  }
3204
- const { git: git2 } = await import("../run-HZTBWGQR.js");
3221
+ const { git: git2 } = await import("../run-74S2NZWB.js");
3205
3222
  const { stdout: headOut } = await git2(
3206
3223
  ["rev-parse", "--abbrev-ref", "HEAD"],
3207
3224
  thread.worktreePath,
@@ -3738,6 +3755,9 @@ function formatWorktreeDirective(thread, opts) {
3738
3755
  );
3739
3756
  return lines.join("\n");
3740
3757
  }
3758
+ function formatWorktreeReminder() {
3759
+ return "Sideboard worktree: stay in this cwd for all file and git work. Push and open PRs against origin, never upstream. Do not edit the main repo checkout.";
3760
+ }
3741
3761
  function formatArtifactDirective() {
3742
3762
  return [
3743
3763
  "Sideboard side column (desktop UI):",
@@ -3758,60 +3778,6 @@ function formatArtifactDirective() {
3758
3778
  "Never say artifacts, CMS UI, or the Files column are unavailable. Prefer present_schema for list/edit/publish; present_files for storage UI; html fences for standalone pages."
3759
3779
  ].join("\n");
3760
3780
  }
3761
- var FILES_BY_AGENT = {
3762
- claude: [
3763
- "CLAUDE.md",
3764
- ".claude/CLAUDE.md",
3765
- "CLAUDE.local.md",
3766
- "AGENTS.md",
3767
- "AGENT.md"
3768
- ],
3769
- codex: ["AGENTS.md", "AGENT.md", ".codex/AGENTS.md", "CLAUDE.md"],
3770
- opencode: ["AGENTS.md", "AGENT.md", "OPENCODE.md", "CLAUDE.md"],
3771
- brightsy: ["AGENTS.md", "AGENT.md", "CLAUDE.md"],
3772
- cursor: ["AGENTS.md", "AGENT.md", ".cursor/rules", "CLAUDE.md"]
3773
- };
3774
- var MAX_CHARS_PER_FILE = 48e3;
3775
- function loadAgentInstructions(worktreePath, agent) {
3776
- const candidates = FILES_BY_AGENT[agent] ?? FILES_BY_AGENT.claude;
3777
- const seen = /* @__PURE__ */ new Set();
3778
- const out = [];
3779
- for (const rel of candidates) {
3780
- if (seen.has(rel)) continue;
3781
- const abs = join10(worktreePath, rel);
3782
- if (!existsSync12(abs)) continue;
3783
- try {
3784
- if (!statSync5(abs).isFile()) continue;
3785
- let content = readFileSync8(abs, "utf8");
3786
- if (!content.trim()) continue;
3787
- if (content.length > MAX_CHARS_PER_FILE) {
3788
- content = `${content.slice(0, MAX_CHARS_PER_FILE)}
3789
-
3790
- \u2026(truncated)`;
3791
- }
3792
- seen.add(rel);
3793
- out.push({ relativePath: rel, content });
3794
- } catch {
3795
- }
3796
- }
3797
- return out;
3798
- }
3799
- function formatAgentInstructions(files) {
3800
- if (files.length === 0) return null;
3801
- const parts = [
3802
- "Project agent instructions (from the worktree \u2014 follow these):",
3803
- ""
3804
- ];
3805
- for (const f of files) {
3806
- parts.push(`## ${f.relativePath}`);
3807
- parts.push("");
3808
- parts.push(f.content.trim());
3809
- parts.push("");
3810
- parts.push("---");
3811
- parts.push("");
3812
- }
3813
- return parts.join("\n").trimEnd();
3814
- }
3815
3781
 
3816
3782
  // src/threads/sync-branch.ts
3817
3783
  async function syncThreadBranchFromGit(threadId) {
@@ -3944,7 +3910,7 @@ var Orchestrator = class {
3944
3910
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
3945
3911
  });
3946
3912
  }
3947
- const { autoCleanupOrphansEnabled } = await import("../app-settings-YBNZ4XSR.js");
3913
+ const { autoCleanupOrphansEnabled } = await import("../app-settings-GJVZGU3M.js");
3948
3914
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
3949
3915
  await cleanupOrphanWorktrees({ repoPaths });
3950
3916
  }
@@ -4101,7 +4067,7 @@ var Orchestrator = class {
4101
4067
  }
4102
4068
  async finishCreateThread(threadId, prompt) {
4103
4069
  await this.runSetupAfterCreate(threadId);
4104
- const { autoRunAfterSetupEnabled } = await import("../app-settings-YBNZ4XSR.js");
4070
+ const { autoRunAfterSetupEnabled } = await import("../app-settings-GJVZGU3M.js");
4105
4071
  if (autoRunAfterSetupEnabled()) {
4106
4072
  try {
4107
4073
  await this.startDev(threadId);
@@ -4342,13 +4308,14 @@ var Orchestrator = class {
4342
4308
  "Files column: MCP present_files (brightsy account storage or memory).",
4343
4309
  "Do not say artifacts/CMS UI are unavailable."
4344
4310
  ].join(" ") : null;
4311
+ const worktreeReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) ? formatWorktreeReminder() : null;
4345
4312
  const agentPrompt = [
4346
4313
  thread.planMode ? PLAN_MODE_INSTRUCTION : null,
4347
4314
  orchestrationReminder,
4315
+ worktreeReminder,
4348
4316
  artifactReminder,
4349
4317
  expandedPrompt
4350
4318
  ].filter(Boolean).join("\n\n");
4351
- const instructionFiles = loadAgentInstructions(thread.worktreePath, thread.agent);
4352
4319
  if (thread.attachments.length > 0) {
4353
4320
  updateThread(threadId, { attachments: [] });
4354
4321
  }
@@ -4372,11 +4339,10 @@ var Orchestrator = class {
4372
4339
  });
4373
4340
  const artifactDirective = isBrightsy ? null : formatArtifactDirective();
4374
4341
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
4375
- const { autoRenameBranchEnabled } = await import("../app-settings-YBNZ4XSR.js");
4342
+ const { autoRenameBranchEnabled } = await import("../app-settings-GJVZGU3M.js");
4376
4343
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
4377
4344
  customPrompt: settings?.prompts?.renameBranch
4378
4345
  }) : null;
4379
- const instructions = fresh.agent === "claude" || isBrightsy ? null : formatAgentInstructions(instructionFiles);
4380
4346
  let seed = null;
4381
4347
  if (!fresh.sessionId) {
4382
4348
  const prior = fresh.messages.slice(0, -1);
@@ -4397,12 +4363,12 @@ var Orchestrator = class {
4397
4363
  });
4398
4364
  }
4399
4365
  }
4400
- const cachedPrefix = [
4366
+ const cachedPrefix = fresh.sessionId ? "" : [
4401
4367
  coordinatorDirective,
4402
4368
  worktreeDirective,
4403
4369
  artifactDirective,
4404
4370
  renameBranchDirective,
4405
- ...fresh.agent === "claude" && fresh.sessionId ? [] : [instructions, seed]
4371
+ seed
4406
4372
  ].filter(Boolean).join("\n\n---\n\n");
4407
4373
  try {
4408
4374
  const stderrTail = [];
@@ -4489,15 +4455,11 @@ var Orchestrator = class {
4489
4455
  const retryThread = this.requireThread(threadId);
4490
4456
  const prior = retryThread.messages.slice(0, -1);
4491
4457
  const retrySeed = buildSessionSeed(prior);
4492
- const retryInstructions = retryThread.agent === "claude" ? null : formatAgentInstructions(
4493
- loadAgentInstructions(retryThread.worktreePath, retryThread.agent)
4494
- );
4495
4458
  const retryPrefix = [
4496
4459
  coordinatorDirective,
4497
4460
  worktreeDirective,
4498
4461
  artifactDirective,
4499
4462
  renameBranchDirective,
4500
- retryInstructions,
4501
4463
  retrySeed
4502
4464
  ].filter(Boolean).join("\n\n---\n\n");
4503
4465
  const retryHandle = await spawnAgentTurn(
@@ -5008,10 +4970,19 @@ var Orchestrator = class {
5008
4970
  this.assertNotGlobal(thread, "Merge PR");
5009
4971
  if (!selector) throw new Error("No pull request linked to this thread");
5010
4972
  const result = await mergePr(cwd, selector);
5011
- if (result.url && result.url !== thread.prUrl) {
5012
- updateThread(thread.id, { prUrl: result.url });
5013
- }
5014
- return result;
4973
+ const state = normalizePrState(result.state) || "MERGED";
4974
+ const metaLike = {
4975
+ number: 0,
4976
+ title: thread.prTitle ?? thread.title,
4977
+ url: result.url || thread.prUrl || "",
4978
+ state,
4979
+ isDraft: false,
4980
+ reviewDecision: null,
4981
+ baseRefName: "",
4982
+ headRefName: ""
4983
+ };
4984
+ await this.persistPrMetaAndMaybeArchive(thread, metaLike);
4985
+ return { url: metaLike.url, state };
5015
4986
  }
5016
4987
  /** Resolve PR selector and optionally persist `prUrl` when found. */
5017
4988
  async withPrSelector(threadRef) {
@@ -5033,19 +5004,55 @@ var Orchestrator = class {
5033
5004
  if (!selector) return null;
5034
5005
  const meta = await getPrMeta(cwd, selector);
5035
5006
  if (meta) {
5036
- const patch = {};
5037
- if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
5038
- if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
5039
- if (Object.keys(patch).length > 0) {
5040
- updateThread(thread.id, patch);
5041
- const latest = this.requireThread(thread.id);
5042
- if (!latest.userSetTitle && meta.title && latest.title !== meta.title) {
5043
- updateThread(thread.id, { title: meta.title });
5044
- }
5045
- }
5007
+ await this.persistPrMetaAndMaybeArchive(thread, meta);
5046
5008
  }
5047
5009
  return meta;
5048
5010
  }
5011
+ /**
5012
+ * Persist PR URL/title/state and Conductor-style auto-archive when the PR
5013
+ * first becomes MERGED.
5014
+ */
5015
+ async persistPrMetaAndMaybeArchive(thread, meta) {
5016
+ const prevState = normalizePrState(thread.prState);
5017
+ const nextState = normalizePrState(meta.state);
5018
+ const patch = {};
5019
+ if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
5020
+ if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
5021
+ if (nextState && nextState !== prevState) patch.prState = nextState;
5022
+ if (thread.skipAutoArchiveOnMerge && nextState && nextState !== "MERGED" && nextState !== "CLOSED") {
5023
+ patch.skipAutoArchiveOnMerge = false;
5024
+ }
5025
+ if (Object.keys(patch).length > 0) {
5026
+ updateThread(thread.id, patch);
5027
+ const latest2 = this.requireThread(thread.id);
5028
+ if (!latest2.userSetTitle && meta.title && latest2.title !== meta.title) {
5029
+ updateThread(thread.id, { title: meta.title });
5030
+ }
5031
+ }
5032
+ const { autoArchiveOnMergeEnabled } = await import("../app-settings-GJVZGU3M.js");
5033
+ const latest = this.requireThread(thread.id);
5034
+ if (!shouldAutoArchiveOnPrMerge({
5035
+ previousPrState: prevState || null,
5036
+ nextPrState: nextState,
5037
+ threadStatus: latest.status,
5038
+ skipAutoArchiveOnMerge: latest.skipAutoArchiveOnMerge,
5039
+ autoArchiveEnabled: autoArchiveOnMergeEnabled(),
5040
+ isGlobal: isGlobalThread(latest)
5041
+ })) {
5042
+ return;
5043
+ }
5044
+ const siblings = threadsSharingWorktree(latest.worktreePath);
5045
+ for (const t of siblings) {
5046
+ const sibPatch = { prState: "MERGED" };
5047
+ if (meta.url && meta.url !== t.prUrl) sibPatch.prUrl = meta.url;
5048
+ if (meta.title && meta.title !== t.prTitle) sibPatch.prTitle = meta.title;
5049
+ if (Object.keys(sibPatch).length > 0) updateThread(t.id, sibPatch);
5050
+ }
5051
+ for (const t of siblings) {
5052
+ if (this.requireThread(t.id).status === "archived") continue;
5053
+ await this.archive(t.id);
5054
+ }
5055
+ }
5049
5056
  async getPrStack(threadRef) {
5050
5057
  const thread = this.requireThread(threadRef);
5051
5058
  if (!thread.worktreePath?.trim()) return null;
@@ -5223,7 +5230,9 @@ var Orchestrator = class {
5223
5230
  const thread = this.requireThread(threadRef);
5224
5231
  this.stop(thread.id);
5225
5232
  if (isGlobalThread(thread)) {
5226
- return setStatus(thread.id, "archived");
5233
+ const archived2 = setStatus(thread.id, "archived");
5234
+ this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
5235
+ return archived2;
5227
5236
  }
5228
5237
  const siblings = threadsSharingWorktree(thread.worktreePath).filter((t) => t.id !== thread.id);
5229
5238
  if (siblings.length === 0) {
@@ -5241,9 +5250,10 @@ var Orchestrator = class {
5241
5250
  await removeWorktree(thread.repoPath, thread.worktreePath);
5242
5251
  }
5243
5252
  const archived = setStatus(thread.id, "archived");
5253
+ this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
5244
5254
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
5245
5255
  try {
5246
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GFPGDGCS.js");
5256
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-RXFXQRRF.js");
5247
5257
  await ensureWorkspace2(thread.repoPath);
5248
5258
  } catch {
5249
5259
  }
@@ -5264,7 +5274,7 @@ var Orchestrator = class {
5264
5274
  await runArchiveScript(thread.repoPath, thread.worktreePath);
5265
5275
  } catch {
5266
5276
  }
5267
- const { deleteBranchOnPurgeEnabled } = await import("../app-settings-YBNZ4XSR.js");
5277
+ const { deleteBranchOnPurgeEnabled } = await import("../app-settings-GJVZGU3M.js");
5268
5278
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
5269
5279
  await removeWorktree(thread.repoPath, thread.worktreePath, {
5270
5280
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -5280,10 +5290,12 @@ var Orchestrator = class {
5280
5290
  if (isGlobalThread(thread)) {
5281
5291
  const { globalAgentCwd } = await import("../paths-2OFB7UJG.js");
5282
5292
  updateThread(thread.id, { worktreePath: globalAgentCwd() });
5283
- return setStatus(thread.id, "idle");
5293
+ const restored2 = setStatus(thread.id, "idle");
5294
+ this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
5295
+ return restored2;
5284
5296
  }
5285
5297
  if (!existsSync13(thread.worktreePath)) {
5286
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-3L2G6PCM.js");
5298
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-S2N63FLV.js");
5287
5299
  const { execa: execa6 } = await import("execa");
5288
5300
  const slug = thread.worktreePath.split("/").pop();
5289
5301
  const dest = thread.worktreePath;
@@ -5293,7 +5305,31 @@ var Orchestrator = class {
5293
5305
  void createThreadWorktree2;
5294
5306
  void slug;
5295
5307
  }
5296
- return setStatus(thread.id, "idle");
5308
+ const restorePatch = {};
5309
+ let alreadyMerged = normalizePrState(thread.prState) === "MERGED";
5310
+ if (!alreadyMerged && thread.prUrl?.trim() && thread.worktreePath?.trim()) {
5311
+ try {
5312
+ const selector = resolvePrSelector(thread);
5313
+ if (selector) {
5314
+ const meta = await getPrMeta(thread.worktreePath, selector);
5315
+ if (meta) {
5316
+ const state = normalizePrState(meta.state);
5317
+ if (meta.url && meta.url !== thread.prUrl) restorePatch.prUrl = meta.url;
5318
+ if (meta.title && meta.title !== thread.prTitle) restorePatch.prTitle = meta.title;
5319
+ if (state) restorePatch.prState = state;
5320
+ if (state === "MERGED") alreadyMerged = true;
5321
+ }
5322
+ }
5323
+ } catch {
5324
+ }
5325
+ }
5326
+ if (alreadyMerged) restorePatch.skipAutoArchiveOnMerge = true;
5327
+ if (Object.keys(restorePatch).length > 0) {
5328
+ updateThread(thread.id, restorePatch);
5329
+ }
5330
+ const restored = setStatus(thread.id, "idle");
5331
+ this.emit({ type: "status_changed", threadId: restored.id, status: restored.status });
5332
+ return restored;
5297
5333
  }
5298
5334
  async attachCommand(threadRef) {
5299
5335
  const thread = this.requireThread(threadRef);
@@ -5475,7 +5511,7 @@ function withTimeout(promise, ms, label) {
5475
5511
  async function startMcpServer() {
5476
5512
  const orch = getOrchestrator();
5477
5513
  try {
5478
- const { maxConcurrentAgents } = await import("../app-settings-YBNZ4XSR.js");
5514
+ const { maxConcurrentAgents } = await import("../app-settings-GJVZGU3M.js");
5479
5515
  orch.setMaxConcurrent(maxConcurrentAgents());
5480
5516
  } catch {
5481
5517
  }
@@ -5566,14 +5602,13 @@ async function startMcpServer() {
5566
5602
  ),
5567
5603
  artifact_id: z.string().optional().describe("Stable id when updating the same artifact across turns")
5568
5604
  },
5569
- async ({ title, type, content, artifact_id }) => {
5605
+ async ({ title, type, artifact_id }) => {
5570
5606
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
5571
5607
  const payload = {
5572
5608
  ok: true,
5573
5609
  artifact_id: id,
5574
5610
  title,
5575
5611
  type,
5576
- content,
5577
5612
  message: "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
5578
5613
  };
5579
5614
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
@@ -5626,7 +5661,6 @@ async function startMcpServer() {
5626
5661
  ok: true,
5627
5662
  path,
5628
5663
  title: title?.trim() || "Plan",
5629
- content,
5630
5664
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
5631
5665
  };
5632
5666
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
@@ -5656,9 +5690,6 @@ async function startMcpServer() {
5656
5690
  datasource: args.datasource ?? (args.resource ? "inline" : "brightsy"),
5657
5691
  resource_id: args.resource_id,
5658
5692
  record_id: args.record_id,
5659
- resource: args.resource,
5660
- record: args.record,
5661
- records: args.records,
5662
5693
  message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
5663
5694
  };
5664
5695
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
@@ -5686,739 +5717,741 @@ async function startMcpServer() {
5686
5717
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5687
5718
  }
5688
5719
  );
5689
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-YBNZ4XSR.js");
5690
- const accountDefaults = resolveThreadDefaults();
5691
- const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
5692
- server.tool(
5693
- "create_thread",
5694
- `Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Then use send_to_thread to chat.`,
5695
- {
5696
- sourceType: z.enum(["branch", "pr", "ticket"]),
5697
- sourceRef: z.string(),
5698
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
5699
- model: z.string().nullable().optional().describe(
5700
- `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
5701
- ),
5702
- repoPath: z.string(),
5703
- title: z.string().optional(),
5704
- parentThreadId: z.string().optional().describe(
5705
- "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5706
- )
5707
- },
5708
- async (args) => {
5709
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
5710
- let parentId = args.parentThreadId?.trim() || "";
5711
- let parent = parentId ? orch.getThread(parentId) : null;
5712
- let parentCorrectedFrom;
5713
- if (envParentId) {
5714
- const envParent = orch.getThread(envParentId);
5715
- if (envParent) {
5716
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
5717
- else if (!parentId) parentCorrectedFrom = void 0;
5718
- parentId = envParentId;
5719
- parent = envParent;
5720
+ if (sideboardMcpProfile() !== "worktree") {
5721
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-GJVZGU3M.js");
5722
+ const accountDefaults = resolveThreadDefaults();
5723
+ const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
5724
+ server.tool(
5725
+ "create_thread",
5726
+ `Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Then use send_to_thread to chat.`,
5727
+ {
5728
+ sourceType: z.enum(["branch", "pr", "ticket"]),
5729
+ sourceRef: z.string(),
5730
+ agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
5731
+ model: z.string().nullable().optional().describe(
5732
+ `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
5733
+ ),
5734
+ repoPath: z.string(),
5735
+ title: z.string().optional(),
5736
+ parentThreadId: z.string().optional().describe(
5737
+ "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5738
+ )
5739
+ },
5740
+ async (args) => {
5741
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
5742
+ let parentId = args.parentThreadId?.trim() || "";
5743
+ let parent = parentId ? orch.getThread(parentId) : null;
5744
+ let parentCorrectedFrom;
5745
+ if (envParentId) {
5746
+ const envParent = orch.getThread(envParentId);
5747
+ if (envParent) {
5748
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
5749
+ else if (!parentId) parentCorrectedFrom = void 0;
5750
+ parentId = envParentId;
5751
+ parent = envParent;
5752
+ }
5720
5753
  }
5721
- }
5722
- if (parentId && !parent) {
5723
- parentCorrectedFrom = parentId;
5724
- parentId = "";
5725
- parent = null;
5726
- }
5727
- if (parentId) {
5728
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
5729
- if (children.length >= MAX_ORCH_THREADS) {
5754
+ if (parentId && !parent) {
5755
+ parentCorrectedFrom = parentId;
5756
+ parentId = "";
5757
+ parent = null;
5758
+ }
5759
+ if (parentId) {
5760
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
5761
+ if (children.length >= MAX_ORCH_THREADS) {
5762
+ return {
5763
+ content: [
5764
+ {
5765
+ type: "text",
5766
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
5767
+ }
5768
+ ],
5769
+ isError: true
5770
+ };
5771
+ }
5772
+ }
5773
+ let agentArg = args.agent;
5774
+ let agentCoercedFrom;
5775
+ const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
5776
+ if (resolvedProbe === "codex") {
5777
+ agentCoercedFrom = agentArg ?? "codex";
5778
+ const accountAgent = resolveNewThreadOptions2({}).agent;
5779
+ agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
5780
+ }
5781
+ const opts = resolveNewThreadOptions2({
5782
+ agent: agentArg,
5783
+ model: args.model
5784
+ });
5785
+ try {
5786
+ const thread = await withTimeout(
5787
+ orch.createThread({
5788
+ sourceType: args.sourceType,
5789
+ sourceRef: args.sourceRef,
5790
+ agent: opts.agent,
5791
+ model: opts.model,
5792
+ effort: opts.effort,
5793
+ fast: opts.fast,
5794
+ repoPath: args.repoPath,
5795
+ title: args.title,
5796
+ parentThreadId: parentId || null
5797
+ }),
5798
+ CREATE_THREAD_TIMEOUT_MS,
5799
+ "create_thread"
5800
+ );
5801
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
5802
+ return {
5803
+ content: [
5804
+ {
5805
+ type: "text",
5806
+ text: JSON.stringify({
5807
+ id: thread.id,
5808
+ title: thread.title,
5809
+ branchName: thread.branchName,
5810
+ worktreePath: thread.worktreePath,
5811
+ agent: thread.agent,
5812
+ model: thread.model,
5813
+ status: thread.status,
5814
+ link: `sideboard://thread/${thread.id}`,
5815
+ parentThreadId: thread.parentThreadId,
5816
+ ...agentCoercedFrom ? {
5817
+ agentCoercedFrom,
5818
+ note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
5819
+ } : {},
5820
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
5821
+ })
5822
+ }
5823
+ ]
5824
+ };
5825
+ } catch (err) {
5826
+ const message = err instanceof Error ? err.message : String(err);
5730
5827
  return {
5731
5828
  content: [
5732
5829
  {
5733
5830
  type: "text",
5734
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
5831
+ text: `create_thread failed: ${message}`
5735
5832
  }
5736
5833
  ],
5737
5834
  isError: true
5738
5835
  };
5739
5836
  }
5740
5837
  }
5741
- let agentArg = args.agent;
5742
- let agentCoercedFrom;
5743
- const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
5744
- if (resolvedProbe === "codex") {
5745
- agentCoercedFrom = agentArg ?? "codex";
5746
- const accountAgent = resolveNewThreadOptions2({}).agent;
5747
- agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
5748
- }
5749
- const opts = resolveNewThreadOptions2({
5750
- agent: agentArg,
5751
- model: args.model
5752
- });
5753
- try {
5754
- const thread = await withTimeout(
5755
- orch.createThread({
5756
- sourceType: args.sourceType,
5757
- sourceRef: args.sourceRef,
5758
- agent: opts.agent,
5759
- model: opts.model,
5760
- effort: opts.effort,
5761
- fast: opts.fast,
5762
- repoPath: args.repoPath,
5763
- title: args.title,
5764
- parentThreadId: parentId || null
5765
- }),
5766
- CREATE_THREAD_TIMEOUT_MS,
5767
- "create_thread"
5768
- );
5769
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
5838
+ );
5839
+ server.tool(
5840
+ "send_to_thread",
5841
+ "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
5842
+ {
5843
+ ref: z.string(),
5844
+ prompt: z.string(),
5845
+ force_stop: z.boolean().optional()
5846
+ },
5847
+ async ({ ref, prompt, force_stop }) => {
5848
+ if (force_stop) {
5849
+ const existing = orch.getThread(ref);
5850
+ if (existing) {
5851
+ orch.stop(ref, { clearQueue: true });
5852
+ }
5853
+ }
5854
+ const thread = await orch.send(ref, prompt);
5770
5855
  return {
5771
5856
  content: [
5772
5857
  {
5773
5858
  type: "text",
5774
5859
  text: JSON.stringify({
5775
5860
  id: thread.id,
5776
- title: thread.title,
5777
- branchName: thread.branchName,
5778
- worktreePath: thread.worktreePath,
5779
- agent: thread.agent,
5780
- model: thread.model,
5781
5861
  status: thread.status,
5782
- link: `sideboard://thread/${thread.id}`,
5783
- parentThreadId: thread.parentThreadId,
5784
- ...agentCoercedFrom ? {
5785
- agentCoercedFrom,
5786
- note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
5787
- } : {},
5788
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
5862
+ queueLength: thread.queue.length,
5863
+ forceStopped: Boolean(force_stop)
5789
5864
  })
5790
5865
  }
5791
5866
  ]
5792
5867
  };
5793
- } catch (err) {
5794
- const message = err instanceof Error ? err.message : String(err);
5868
+ }
5869
+ );
5870
+ server.tool(
5871
+ "wait_for_turn",
5872
+ "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
5873
+ {
5874
+ ref: z.string(),
5875
+ timeoutMs: z.number().optional()
5876
+ },
5877
+ async ({ ref, timeoutMs }) => {
5878
+ const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
5879
+ const result = orch.getTurnResult(thread.id);
5795
5880
  return {
5796
5881
  content: [
5797
5882
  {
5798
5883
  type: "text",
5799
- text: `create_thread failed: ${message}`
5884
+ text: JSON.stringify({
5885
+ id: thread.id,
5886
+ status: result.status,
5887
+ text: result.text
5888
+ })
5800
5889
  }
5801
- ],
5802
- isError: true
5890
+ ]
5803
5891
  };
5804
5892
  }
5805
- }
5806
- );
5807
- server.tool(
5808
- "send_to_thread",
5809
- "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
5810
- {
5811
- ref: z.string(),
5812
- prompt: z.string(),
5813
- force_stop: z.boolean().optional()
5814
- },
5815
- async ({ ref, prompt, force_stop }) => {
5816
- if (force_stop) {
5817
- const existing = orch.getThread(ref);
5818
- if (existing) {
5819
- orch.stop(ref, { clearQueue: true });
5820
- }
5893
+ );
5894
+ server.tool(
5895
+ "get_turn_result",
5896
+ "Final assistant message only (not full transcript)",
5897
+ { ref: z.string() },
5898
+ async ({ ref }) => {
5899
+ const result = orch.getTurnResult(ref);
5900
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
5821
5901
  }
5822
- const thread = await orch.send(ref, prompt);
5823
- return {
5824
- content: [
5825
- {
5826
- type: "text",
5827
- text: JSON.stringify({
5828
- id: thread.id,
5829
- status: thread.status,
5830
- queueLength: thread.queue.length,
5831
- forceStopped: Boolean(force_stop)
5832
- })
5833
- }
5834
- ]
5835
- };
5836
- }
5837
- );
5838
- server.tool(
5839
- "wait_for_turn",
5840
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
5841
- {
5842
- ref: z.string(),
5843
- timeoutMs: z.number().optional()
5844
- },
5845
- async ({ ref, timeoutMs }) => {
5846
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
5847
- const result = orch.getTurnResult(thread.id);
5848
- return {
5849
- content: [
5850
- {
5851
- type: "text",
5852
- text: JSON.stringify({
5853
- id: thread.id,
5854
- status: result.status,
5855
- text: result.text
5856
- })
5857
- }
5858
- ]
5859
- };
5860
- }
5861
- );
5862
- server.tool(
5863
- "get_turn_result",
5864
- "Final assistant message only (not full transcript)",
5865
- { ref: z.string() },
5866
- async ({ ref }) => {
5867
- const result = orch.getTurnResult(ref);
5868
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
5869
- }
5870
- );
5871
- server.tool(
5872
- "stop_thread",
5873
- "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
5874
- {
5875
- ref: z.string(),
5876
- force: z.boolean().optional()
5877
- },
5878
- async ({ ref, force }) => {
5879
- const t = orch.getThread(ref);
5880
- if (!t) {
5902
+ );
5903
+ server.tool(
5904
+ "stop_thread",
5905
+ "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
5906
+ {
5907
+ ref: z.string(),
5908
+ force: z.boolean().optional()
5909
+ },
5910
+ async ({ ref, force }) => {
5911
+ const t = orch.getThread(ref);
5912
+ if (!t) {
5913
+ return {
5914
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
5915
+ isError: true
5916
+ };
5917
+ }
5918
+ const clearQueue = force !== false;
5919
+ const hadQueued = t.queue.length > 0;
5920
+ const stopped = orch.stop(ref, { clearQueue });
5881
5921
  return {
5882
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
5883
- isError: true
5922
+ content: [
5923
+ {
5924
+ type: "text",
5925
+ text: JSON.stringify({
5926
+ id: stopped.id,
5927
+ status: stopped.status,
5928
+ clearedQueue: clearQueue && hadQueued
5929
+ })
5930
+ }
5931
+ ]
5884
5932
  };
5885
5933
  }
5886
- const clearQueue = force !== false;
5887
- const hadQueued = t.queue.length > 0;
5888
- const stopped = orch.stop(ref, { clearQueue });
5889
- return {
5890
- content: [
5891
- {
5892
- type: "text",
5893
- text: JSON.stringify({
5894
- id: stopped.id,
5895
- status: stopped.status,
5896
- clearedQueue: clearQueue && hadQueued
5897
- })
5898
- }
5899
- ]
5900
- };
5901
- }
5902
- );
5903
- server.tool(
5904
- "archive_thread",
5905
- "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Cannot archive the cloud coordinator \u2014 use the Sideboard UI for that.",
5906
- { ref: z.string() },
5907
- async ({ ref }) => {
5908
- const t = orch.getThread(ref);
5909
- if (!t) {
5934
+ );
5935
+ server.tool(
5936
+ "archive_thread",
5937
+ "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Cannot archive the cloud coordinator \u2014 use the Sideboard UI for that.",
5938
+ { ref: z.string() },
5939
+ async ({ ref }) => {
5940
+ const t = orch.getThread(ref);
5941
+ if (!t) {
5942
+ return {
5943
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
5944
+ isError: true
5945
+ };
5946
+ }
5947
+ const blocked = mcpArchiveBlockedReason(t);
5948
+ if (blocked) {
5949
+ return {
5950
+ content: [{ type: "text", text: blocked }],
5951
+ isError: true
5952
+ };
5953
+ }
5954
+ const archived = await orch.archive(ref);
5910
5955
  return {
5911
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
5912
- isError: true
5956
+ content: [
5957
+ {
5958
+ type: "text",
5959
+ text: JSON.stringify({
5960
+ id: archived.id,
5961
+ status: archived.status
5962
+ })
5963
+ }
5964
+ ]
5913
5965
  };
5914
5966
  }
5915
- const blocked = mcpArchiveBlockedReason(t);
5916
- if (blocked) {
5917
- return {
5918
- content: [{ type: "text", text: blocked }],
5919
- isError: true
5920
- };
5967
+ );
5968
+ server.tool(
5969
+ "restore_thread",
5970
+ "Restore an archived thread (recreates worktree from branch when needed)",
5971
+ { ref: z.string() },
5972
+ async ({ ref }) => {
5973
+ try {
5974
+ const restored = await orch.restore(ref);
5975
+ return {
5976
+ content: [
5977
+ {
5978
+ type: "text",
5979
+ text: JSON.stringify({
5980
+ id: restored.id,
5981
+ status: restored.status,
5982
+ worktreePath: restored.worktreePath
5983
+ })
5984
+ }
5985
+ ]
5986
+ };
5987
+ } catch (err) {
5988
+ const message = err instanceof Error ? err.message : String(err);
5989
+ return { content: [{ type: "text", text: message }], isError: true };
5990
+ }
5921
5991
  }
5922
- const archived = await orch.archive(ref);
5923
- return {
5924
- content: [
5925
- {
5926
- type: "text",
5927
- text: JSON.stringify({
5928
- id: archived.id,
5929
- status: archived.status
5930
- })
5931
- }
5932
- ]
5933
- };
5934
- }
5935
- );
5936
- server.tool(
5937
- "restore_thread",
5938
- "Restore an archived thread (recreates worktree from branch when needed)",
5939
- { ref: z.string() },
5940
- async ({ ref }) => {
5941
- try {
5942
- const restored = await orch.restore(ref);
5992
+ );
5993
+ server.tool(
5994
+ "get_diff",
5995
+ "Compact diff summary (capped hunks, paginated)",
5996
+ {
5997
+ ref: z.string(),
5998
+ maxFiles: z.number().optional()
5999
+ },
6000
+ async ({ ref, maxFiles }) => {
6001
+ const summary = await orch.diffSummary(ref);
5943
6002
  return {
5944
6003
  content: [
5945
6004
  {
5946
6005
  type: "text",
5947
6006
  text: JSON.stringify({
5948
- id: restored.id,
5949
- status: restored.status,
5950
- worktreePath: restored.worktreePath
6007
+ ...summary,
6008
+ files: summary.files.slice(0, maxFiles ?? 10)
5951
6009
  })
5952
6010
  }
5953
6011
  ]
5954
6012
  };
5955
- } catch (err) {
5956
- const message = err instanceof Error ? err.message : String(err);
5957
- return { content: [{ type: "text", text: message }], isError: true };
5958
6013
  }
5959
- }
5960
- );
5961
- server.tool(
5962
- "get_diff",
5963
- "Compact diff summary (capped hunks, paginated)",
5964
- {
5965
- ref: z.string(),
5966
- maxFiles: z.number().optional()
5967
- },
5968
- async ({ ref, maxFiles }) => {
5969
- const summary = await orch.diffSummary(ref);
5970
- return {
5971
- content: [
5972
- {
5973
- type: "text",
5974
- text: JSON.stringify({
5975
- ...summary,
5976
- files: summary.files.slice(0, maxFiles ?? 10)
5977
- })
6014
+ );
6015
+ server.tool(
6016
+ "request_review",
6017
+ 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect 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.',
6018
+ { ref: z.string().describe("Worktree thread id/ref to review") },
6019
+ async ({ ref }) => {
6020
+ try {
6021
+ const tab = await orch.requestReview(ref);
6022
+ const from = orch.getThread(ref);
6023
+ return {
6024
+ content: [
6025
+ {
6026
+ type: "text",
6027
+ text: JSON.stringify({
6028
+ id: tab.id,
6029
+ title: tab.title,
6030
+ status: tab.status,
6031
+ fromThreadId: from?.id ?? ref,
6032
+ link: `sideboard://thread/${tab.id}`
6033
+ })
6034
+ }
6035
+ ]
6036
+ };
6037
+ } catch (err) {
6038
+ const message = err instanceof Error ? err.message : String(err);
6039
+ return { content: [{ type: "text", text: message }], isError: true };
6040
+ }
6041
+ }
6042
+ );
6043
+ const agentEnum = z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6044
+ server.tool(
6045
+ "list_models",
6046
+ "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.",
6047
+ {
6048
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
6049
+ },
6050
+ async ({ agent }) => {
6051
+ try {
6052
+ const catalogs = await listModelsForAgent(agent);
6053
+ return {
6054
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
6055
+ };
6056
+ } catch (err) {
6057
+ const message = err instanceof Error ? err.message : String(err);
6058
+ return { content: [{ type: "text", text: message }], isError: true };
6059
+ }
6060
+ }
6061
+ );
6062
+ server.tool(
6063
+ "fork_worktree",
6064
+ "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.",
6065
+ {
6066
+ ref: z.string().describe("Worktree thread id/ref to fork"),
6067
+ through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6068
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6069
+ model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6070
+ title: z.string().optional()
6071
+ },
6072
+ async ({ ref, through_index, agent, model, title }) => {
6073
+ try {
6074
+ const source = orch.getThread(ref);
6075
+ if (source) await orch.reconcile(source.repoPath, { drainQueues: false });
6076
+ const thread = await orch.forkThreadWorktree({
6077
+ threadId: ref,
6078
+ throughIndex: through_index,
6079
+ agent,
6080
+ model,
6081
+ title
6082
+ });
6083
+ return {
6084
+ content: [
6085
+ {
6086
+ type: "text",
6087
+ text: JSON.stringify({
6088
+ id: thread.id,
6089
+ title: thread.title,
6090
+ status: thread.status,
6091
+ agent: thread.agent,
6092
+ model: thread.model,
6093
+ branchName: thread.branchName,
6094
+ worktreePath: thread.worktreePath,
6095
+ fromThreadId: source?.id ?? ref,
6096
+ link: `sideboard://thread/${thread.id}`
6097
+ })
6098
+ }
6099
+ ]
6100
+ };
6101
+ } catch (err) {
6102
+ const message = err instanceof Error ? err.message : String(err);
6103
+ return { content: [{ type: "text", text: message }], isError: true };
6104
+ }
6105
+ }
6106
+ );
6107
+ server.tool(
6108
+ "fork_chat",
6109
+ "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. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). 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.",
6110
+ {
6111
+ ref: z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6112
+ through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6113
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6114
+ model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6115
+ title: z.string().optional()
6116
+ },
6117
+ async ({ ref, through_index, agent, model, title }) => {
6118
+ try {
6119
+ const source = orch.getThread(ref);
6120
+ if (!source) {
6121
+ return {
6122
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
6123
+ isError: true
6124
+ };
5978
6125
  }
5979
- ]
5980
- };
5981
- }
5982
- );
5983
- server.tool(
5984
- "request_review",
5985
- 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect 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.',
5986
- { ref: z.string().describe("Worktree thread id/ref to review") },
5987
- async ({ ref }) => {
5988
- try {
5989
- const tab = await orch.requestReview(ref);
5990
- const from = orch.getThread(ref);
6126
+ const tab = orch.forkChatTab({
6127
+ threadId: source.id,
6128
+ throughIndex: through_index,
6129
+ agent,
6130
+ model,
6131
+ title
6132
+ });
6133
+ return {
6134
+ content: [
6135
+ {
6136
+ type: "text",
6137
+ text: JSON.stringify({
6138
+ id: tab.id,
6139
+ title: tab.title,
6140
+ status: tab.status,
6141
+ agent: tab.agent,
6142
+ model: tab.model,
6143
+ sourceType: tab.sourceType,
6144
+ worktreePath: tab.worktreePath,
6145
+ fromThreadId: source.id,
6146
+ link: `sideboard://thread/${tab.id}`
6147
+ })
6148
+ }
6149
+ ]
6150
+ };
6151
+ } catch (err) {
6152
+ const message = err instanceof Error ? err.message : String(err);
6153
+ return { content: [{ type: "text", text: message }], isError: true };
6154
+ }
6155
+ }
6156
+ );
6157
+ server.tool(
6158
+ "run_dev_script",
6159
+ "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
6160
+ {
6161
+ ref: z.string(),
6162
+ name: z.string().optional()
6163
+ },
6164
+ async ({ ref, name }) => {
6165
+ const result = await orch.startDev(ref, name);
5991
6166
  return {
5992
6167
  content: [
5993
6168
  {
5994
6169
  type: "text",
5995
6170
  text: JSON.stringify({
5996
- id: tab.id,
5997
- title: tab.title,
5998
- status: tab.status,
5999
- fromThreadId: from?.id ?? ref,
6000
- link: `sideboard://thread/${tab.id}`
6171
+ port: result.port,
6172
+ scriptName: result.scriptName,
6173
+ ports: result.ports,
6174
+ url: `http://localhost:${result.port}`
6001
6175
  })
6002
6176
  }
6003
6177
  ]
6004
6178
  };
6005
- } catch (err) {
6006
- const message = err instanceof Error ? err.message : String(err);
6007
- return { content: [{ type: "text", text: message }], isError: true };
6008
6179
  }
6009
- }
6010
- );
6011
- const agentEnum = z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6012
- server.tool(
6013
- "list_models",
6014
- "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.",
6015
- {
6016
- agent: agentEnum.optional().describe("Limit to one agent; omit for all")
6017
- },
6018
- async ({ agent }) => {
6019
- try {
6020
- const catalogs = await listModelsForAgent(agent);
6180
+ );
6181
+ server.tool(
6182
+ "list_run_scripts",
6183
+ "List named run scripts available for a thread",
6184
+ { ref: z.string() },
6185
+ async ({ ref }) => {
6186
+ const scripts = orch.listThreadRunScripts(ref);
6187
+ const active = orch.getActiveRuns(ref);
6021
6188
  return {
6022
- content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
6189
+ content: [
6190
+ {
6191
+ type: "text",
6192
+ text: JSON.stringify({ scripts, active }, null, 2)
6193
+ }
6194
+ ]
6023
6195
  };
6024
- } catch (err) {
6025
- const message = err instanceof Error ? err.message : String(err);
6026
- return { content: [{ type: "text", text: message }], isError: true };
6027
6196
  }
6028
- }
6029
- );
6030
- server.tool(
6031
- "fork_worktree",
6032
- "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.",
6033
- {
6034
- ref: z.string().describe("Worktree thread id/ref to fork"),
6035
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6036
- agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6037
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6038
- title: z.string().optional()
6039
- },
6040
- async ({ ref, through_index, agent, model, title }) => {
6041
- try {
6042
- const source = orch.getThread(ref);
6043
- if (source) await orch.reconcile(source.repoPath, { drainQueues: false });
6044
- const thread = await orch.forkThreadWorktree({
6045
- threadId: ref,
6046
- throughIndex: through_index,
6047
- agent,
6048
- model,
6049
- title
6197
+ );
6198
+ server.tool(
6199
+ "stop_dev_script",
6200
+ "Stop a running script for a thread (all scripts if name omitted)",
6201
+ {
6202
+ ref: z.string(),
6203
+ name: z.string().optional()
6204
+ },
6205
+ async ({ ref, name }) => {
6206
+ orch.stopDev(ref, name);
6207
+ return { content: [{ type: "text", text: "ok" }] };
6208
+ }
6209
+ );
6210
+ server.tool(
6211
+ "run_setup",
6212
+ "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
6213
+ { ref: z.string() },
6214
+ async ({ ref }) => {
6215
+ const result = await orch.runSetup(ref);
6216
+ return {
6217
+ content: [{ type: "text", text: JSON.stringify(result) }]
6218
+ };
6219
+ }
6220
+ );
6221
+ server.tool(
6222
+ "add_workspace",
6223
+ "Register a git repo as a Sideboard workspace",
6224
+ { repoPath: z.string() },
6225
+ async ({ repoPath }) => {
6226
+ const ws = await orch.addWorkspace(repoPath);
6227
+ return { content: [{ type: "text", text: JSON.stringify(ws) }] };
6228
+ }
6229
+ );
6230
+ server.tool(
6231
+ "remove_workspace",
6232
+ "Unregister a Sideboard workspace (does not archive threads)",
6233
+ { repoPath: z.string() },
6234
+ async ({ repoPath }) => {
6235
+ orch.removeWorkspace(repoPath);
6236
+ return { content: [{ type: "text", text: "ok" }] };
6237
+ }
6238
+ );
6239
+ server.tool(
6240
+ "fanout",
6241
+ "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
6242
+ {
6243
+ prompt: z.string(),
6244
+ agents: z.array(z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6245
+ repoPath: z.string(),
6246
+ sourceType: z.enum(["branch", "pr", "ticket"]).optional(),
6247
+ sourceRef: z.string().optional(),
6248
+ title: z.string().optional()
6249
+ },
6250
+ async (args) => {
6251
+ const threads = await orch.bestOfN(args);
6252
+ return {
6253
+ content: [
6254
+ {
6255
+ type: "text",
6256
+ text: JSON.stringify(
6257
+ threads.map((t) => ({
6258
+ id: t.id,
6259
+ agent: t.agent,
6260
+ branchName: t.branchName,
6261
+ worktreePath: t.worktreePath
6262
+ })),
6263
+ null,
6264
+ 2
6265
+ )
6266
+ }
6267
+ ]
6268
+ };
6269
+ }
6270
+ );
6271
+ server.tool(
6272
+ "list_branches",
6273
+ "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
6274
+ {
6275
+ repoPath: z.string(),
6276
+ unmergedOnly: z.boolean().optional()
6277
+ },
6278
+ async ({ repoPath, unmergedOnly }) => {
6279
+ const root = await resolveRepoRoot(repoPath);
6280
+ const branches = await listBranches(root, {
6281
+ unmergedOnly: unmergedOnly !== false
6050
6282
  });
6051
6283
  return {
6052
6284
  content: [
6053
6285
  {
6054
6286
  type: "text",
6055
- text: JSON.stringify({
6056
- id: thread.id,
6057
- title: thread.title,
6058
- status: thread.status,
6059
- agent: thread.agent,
6060
- model: thread.model,
6061
- branchName: thread.branchName,
6062
- worktreePath: thread.worktreePath,
6063
- fromThreadId: source?.id ?? ref,
6064
- link: `sideboard://thread/${thread.id}`
6065
- })
6287
+ text: branches.map((b) => `${b.current ? "*" : " "} ${b.name}`).join("\n")
6066
6288
  }
6067
6289
  ]
6068
6290
  };
6069
- } catch (err) {
6070
- const message = err instanceof Error ? err.message : String(err);
6071
- return { content: [{ type: "text", text: message }], isError: true };
6072
6291
  }
6073
- }
6074
- );
6075
- server.tool(
6076
- "fork_chat",
6077
- "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. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). 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.",
6078
- {
6079
- ref: z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6080
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6081
- agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6082
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6083
- title: z.string().optional()
6084
- },
6085
- async ({ ref, through_index, agent, model, title }) => {
6086
- try {
6087
- const source = orch.getThread(ref);
6088
- if (!source) {
6089
- return {
6090
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
6091
- isError: true
6092
- };
6093
- }
6094
- const tab = orch.forkChatTab({
6095
- threadId: source.id,
6096
- throughIndex: through_index,
6097
- agent,
6098
- model,
6099
- title
6292
+ );
6293
+ server.tool(
6294
+ "list_prs",
6295
+ "List open GitHub PRs for a registered workspace. Pass repoPath from list_workspaces (uses gh against that repo remote). Then create_thread with sourceType=pr.",
6296
+ { repoPath: z.string() },
6297
+ async ({ repoPath }) => {
6298
+ const root = await resolveRepoRoot(repoPath);
6299
+ const prs = await listPrs(root);
6300
+ return {
6301
+ content: [
6302
+ {
6303
+ type: "text",
6304
+ text: prs.map(
6305
+ (p) => `#${p.number} ${p.title} (${p.headRefName})${p.isCrossRepository ? " [fork]" : ""}`
6306
+ ).join("\n")
6307
+ }
6308
+ ]
6309
+ };
6310
+ }
6311
+ );
6312
+ server.tool(
6313
+ "get_pr_stack",
6314
+ "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before mergePr on stacked PRs.",
6315
+ { ref: z.string() },
6316
+ async ({ ref }) => {
6317
+ const stack = await orch.getPrStack(ref);
6318
+ return {
6319
+ content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
6320
+ };
6321
+ }
6322
+ );
6323
+ server.tool(
6324
+ "open_pr_stack_layers",
6325
+ "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6326
+ {
6327
+ ref: z.string(),
6328
+ layer: z.number().int().positive().optional()
6329
+ },
6330
+ async ({ ref, layer }) => {
6331
+ const result = await orch.openPrStackLayers(ref, { layer });
6332
+ return {
6333
+ content: [
6334
+ {
6335
+ type: "text",
6336
+ text: JSON.stringify(
6337
+ {
6338
+ stackNumber: result.stack.stackNumber,
6339
+ trunk: result.stack.trunk,
6340
+ threads: result.threads.map((t) => ({
6341
+ id: t.id,
6342
+ title: t.title,
6343
+ branchName: t.branchName,
6344
+ stackLayer: t.stackLayer,
6345
+ worktreePath: t.worktreePath,
6346
+ prUrl: t.prUrl,
6347
+ link: `sideboard://thread/${t.id}`
6348
+ }))
6349
+ },
6350
+ null,
6351
+ 2
6352
+ )
6353
+ }
6354
+ ]
6355
+ };
6356
+ }
6357
+ );
6358
+ server.tool(
6359
+ "add_stack_layer",
6360
+ "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6361
+ {
6362
+ ref: z.string(),
6363
+ branchName: z.string(),
6364
+ title: z.string().optional()
6365
+ },
6366
+ async ({ ref, branchName, title }) => {
6367
+ const result = await orch.addStackLayer(ref, branchName, { title });
6368
+ return {
6369
+ content: [
6370
+ {
6371
+ type: "text",
6372
+ text: JSON.stringify(
6373
+ {
6374
+ id: result.thread.id,
6375
+ title: result.thread.title,
6376
+ branchName: result.thread.branchName,
6377
+ stackLayer: result.thread.stackLayer,
6378
+ worktreePath: result.thread.worktreePath,
6379
+ link: `sideboard://thread/${result.thread.id}`
6380
+ },
6381
+ null,
6382
+ 2
6383
+ )
6384
+ }
6385
+ ]
6386
+ };
6387
+ }
6388
+ );
6389
+ server.tool(
6390
+ "create_pr_stack",
6391
+ "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6392
+ {
6393
+ repoPath: z.string(),
6394
+ branches: z.array(z.string()).min(1),
6395
+ agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6396
+ base: z.string().optional(),
6397
+ title: z.string().optional()
6398
+ },
6399
+ async (args) => {
6400
+ const result = await orch.createPrStack({
6401
+ repoPath: args.repoPath,
6402
+ branches: args.branches,
6403
+ agent: args.agent,
6404
+ base: args.base,
6405
+ title: args.title
6100
6406
  });
6101
6407
  return {
6102
6408
  content: [
6103
6409
  {
6104
6410
  type: "text",
6105
- text: JSON.stringify({
6106
- id: tab.id,
6107
- title: tab.title,
6108
- status: tab.status,
6109
- agent: tab.agent,
6110
- model: tab.model,
6111
- sourceType: tab.sourceType,
6112
- worktreePath: tab.worktreePath,
6113
- fromThreadId: source.id,
6114
- link: `sideboard://thread/${tab.id}`
6115
- })
6411
+ text: JSON.stringify(
6412
+ {
6413
+ stackNumber: result.stack.stackNumber,
6414
+ trunk: result.stack.trunk,
6415
+ threads: result.threads.map((t) => ({
6416
+ id: t.id,
6417
+ title: t.title,
6418
+ branchName: t.branchName,
6419
+ stackLayer: t.stackLayer,
6420
+ worktreePath: t.worktreePath,
6421
+ link: `sideboard://thread/${t.id}`
6422
+ }))
6423
+ },
6424
+ null,
6425
+ 2
6426
+ )
6116
6427
  }
6117
6428
  ]
6118
6429
  };
6119
- } catch (err) {
6120
- const message = err instanceof Error ? err.message : String(err);
6121
- return { content: [{ type: "text", text: message }], isError: true };
6122
6430
  }
6123
- }
6124
- );
6125
- server.tool(
6126
- "run_dev_script",
6127
- "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
6128
- {
6129
- ref: z.string(),
6130
- name: z.string().optional()
6131
- },
6132
- async ({ ref, name }) => {
6133
- const result = await orch.startDev(ref, name);
6134
- return {
6135
- content: [
6136
- {
6137
- type: "text",
6138
- text: JSON.stringify({
6139
- port: result.port,
6140
- scriptName: result.scriptName,
6141
- ports: result.ports,
6142
- url: `http://localhost:${result.port}`
6143
- })
6144
- }
6145
- ]
6146
- };
6147
- }
6148
- );
6149
- server.tool(
6150
- "list_run_scripts",
6151
- "List named run scripts available for a thread",
6152
- { ref: z.string() },
6153
- async ({ ref }) => {
6154
- const scripts = orch.listThreadRunScripts(ref);
6155
- const active = orch.getActiveRuns(ref);
6156
- return {
6157
- content: [
6158
- {
6159
- type: "text",
6160
- text: JSON.stringify({ scripts, active }, null, 2)
6161
- }
6162
- ]
6163
- };
6164
- }
6165
- );
6166
- server.tool(
6167
- "stop_dev_script",
6168
- "Stop a running script for a thread (all scripts if name omitted)",
6169
- {
6170
- ref: z.string(),
6171
- name: z.string().optional()
6172
- },
6173
- async ({ ref, name }) => {
6174
- orch.stopDev(ref, name);
6175
- return { content: [{ type: "text", text: "ok" }] };
6176
- }
6177
- );
6178
- server.tool(
6179
- "run_setup",
6180
- "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
6181
- { ref: z.string() },
6182
- async ({ ref }) => {
6183
- const result = await orch.runSetup(ref);
6184
- return {
6185
- content: [{ type: "text", text: JSON.stringify(result) }]
6186
- };
6187
- }
6188
- );
6189
- server.tool(
6190
- "add_workspace",
6191
- "Register a git repo as a Sideboard workspace",
6192
- { repoPath: z.string() },
6193
- async ({ repoPath }) => {
6194
- const ws = await orch.addWorkspace(repoPath);
6195
- return { content: [{ type: "text", text: JSON.stringify(ws) }] };
6196
- }
6197
- );
6198
- server.tool(
6199
- "remove_workspace",
6200
- "Unregister a Sideboard workspace (does not archive threads)",
6201
- { repoPath: z.string() },
6202
- async ({ repoPath }) => {
6203
- orch.removeWorkspace(repoPath);
6204
- return { content: [{ type: "text", text: "ok" }] };
6205
- }
6206
- );
6207
- server.tool(
6208
- "fanout",
6209
- "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
6210
- {
6211
- prompt: z.string(),
6212
- agents: z.array(z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6213
- repoPath: z.string(),
6214
- sourceType: z.enum(["branch", "pr", "ticket"]).optional(),
6215
- sourceRef: z.string().optional(),
6216
- title: z.string().optional()
6217
- },
6218
- async (args) => {
6219
- const threads = await orch.bestOfN(args);
6220
- return {
6221
- content: [
6222
- {
6223
- type: "text",
6224
- text: JSON.stringify(
6225
- threads.map((t) => ({
6226
- id: t.id,
6227
- agent: t.agent,
6228
- branchName: t.branchName,
6229
- worktreePath: t.worktreePath
6230
- })),
6231
- null,
6232
- 2
6233
- )
6234
- }
6235
- ]
6236
- };
6237
- }
6238
- );
6239
- server.tool(
6240
- "list_branches",
6241
- "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
6242
- {
6243
- repoPath: z.string(),
6244
- unmergedOnly: z.boolean().optional()
6245
- },
6246
- async ({ repoPath, unmergedOnly }) => {
6247
- const root = await resolveRepoRoot(repoPath);
6248
- const branches = await listBranches(root, {
6249
- unmergedOnly: unmergedOnly !== false
6250
- });
6251
- return {
6252
- content: [
6253
- {
6254
- type: "text",
6255
- text: branches.map((b) => `${b.current ? "*" : " "} ${b.name}`).join("\n")
6256
- }
6257
- ]
6258
- };
6259
- }
6260
- );
6261
- server.tool(
6262
- "list_prs",
6263
- "List open GitHub PRs for a registered workspace. Pass repoPath from list_workspaces (uses gh against that repo remote). Then create_thread with sourceType=pr.",
6264
- { repoPath: z.string() },
6265
- async ({ repoPath }) => {
6266
- const root = await resolveRepoRoot(repoPath);
6267
- const prs = await listPrs(root);
6268
- return {
6269
- content: [
6270
- {
6271
- type: "text",
6272
- text: prs.map(
6273
- (p) => `#${p.number} ${p.title} (${p.headRefName})${p.isCrossRepository ? " [fork]" : ""}`
6274
- ).join("\n")
6275
- }
6276
- ]
6277
- };
6278
- }
6279
- );
6280
- server.tool(
6281
- "get_pr_stack",
6282
- "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before mergePr on stacked PRs.",
6283
- { ref: z.string() },
6284
- async ({ ref }) => {
6285
- const stack = await orch.getPrStack(ref);
6286
- return {
6287
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
6288
- };
6289
- }
6290
- );
6291
- server.tool(
6292
- "open_pr_stack_layers",
6293
- "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6294
- {
6295
- ref: z.string(),
6296
- layer: z.number().int().positive().optional()
6297
- },
6298
- async ({ ref, layer }) => {
6299
- const result = await orch.openPrStackLayers(ref, { layer });
6300
- return {
6301
- content: [
6302
- {
6303
- type: "text",
6304
- text: JSON.stringify(
6305
- {
6306
- stackNumber: result.stack.stackNumber,
6307
- trunk: result.stack.trunk,
6308
- threads: result.threads.map((t) => ({
6309
- id: t.id,
6310
- title: t.title,
6311
- branchName: t.branchName,
6312
- stackLayer: t.stackLayer,
6313
- worktreePath: t.worktreePath,
6314
- prUrl: t.prUrl,
6315
- link: `sideboard://thread/${t.id}`
6316
- }))
6317
- },
6318
- null,
6319
- 2
6320
- )
6321
- }
6322
- ]
6323
- };
6324
- }
6325
- );
6326
- server.tool(
6327
- "add_stack_layer",
6328
- "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6329
- {
6330
- ref: z.string(),
6331
- branchName: z.string(),
6332
- title: z.string().optional()
6333
- },
6334
- async ({ ref, branchName, title }) => {
6335
- const result = await orch.addStackLayer(ref, branchName, { title });
6336
- return {
6337
- content: [
6338
- {
6339
- type: "text",
6340
- text: JSON.stringify(
6341
- {
6342
- id: result.thread.id,
6343
- title: result.thread.title,
6344
- branchName: result.thread.branchName,
6345
- stackLayer: result.thread.stackLayer,
6346
- worktreePath: result.thread.worktreePath,
6347
- link: `sideboard://thread/${result.thread.id}`
6348
- },
6349
- null,
6350
- 2
6351
- )
6352
- }
6353
- ]
6354
- };
6355
- }
6356
- );
6357
- server.tool(
6358
- "create_pr_stack",
6359
- "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6360
- {
6361
- repoPath: z.string(),
6362
- branches: z.array(z.string()).min(1),
6363
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6364
- base: z.string().optional(),
6365
- title: z.string().optional()
6366
- },
6367
- async (args) => {
6368
- const result = await orch.createPrStack({
6369
- repoPath: args.repoPath,
6370
- branches: args.branches,
6371
- agent: args.agent,
6372
- base: args.base,
6373
- title: args.title
6374
- });
6375
- return {
6376
- content: [
6377
- {
6378
- type: "text",
6379
- text: JSON.stringify(
6380
- {
6381
- stackNumber: result.stack.stackNumber,
6382
- trunk: result.stack.trunk,
6383
- threads: result.threads.map((t) => ({
6384
- id: t.id,
6385
- title: t.title,
6386
- branchName: t.branchName,
6387
- stackLayer: t.stackLayer,
6388
- worktreePath: t.worktreePath,
6389
- link: `sideboard://thread/${t.id}`
6390
- }))
6391
- },
6392
- null,
6393
- 2
6394
- )
6395
- }
6396
- ]
6397
- };
6398
- }
6399
- );
6400
- server.tool(
6401
- "list_issues",
6402
- "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
6403
- { repoPath: z.string() },
6404
- async ({ repoPath }) => {
6405
- const root = await resolveRepoRoot(repoPath);
6406
- const result = await listIssues(root);
6407
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6408
- }
6409
- );
6410
- server.tool(
6411
- "list_linear_issues",
6412
- "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
6413
- {
6414
- agent: z.enum(["claude", "codex", "opencode"]),
6415
- repoPath: z.string()
6416
- },
6417
- async ({ agent, repoPath }) => {
6418
- const issues = await listLinearIssues(agent, repoPath);
6419
- return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
6420
- }
6421
- );
6431
+ );
6432
+ server.tool(
6433
+ "list_issues",
6434
+ "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
6435
+ { repoPath: z.string() },
6436
+ async ({ repoPath }) => {
6437
+ const root = await resolveRepoRoot(repoPath);
6438
+ const result = await listIssues(root);
6439
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6440
+ }
6441
+ );
6442
+ server.tool(
6443
+ "list_linear_issues",
6444
+ "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
6445
+ {
6446
+ agent: z.enum(["claude", "codex", "opencode"]),
6447
+ repoPath: z.string()
6448
+ },
6449
+ async ({ agent, repoPath }) => {
6450
+ const issues = await listLinearIssues(agent, repoPath);
6451
+ return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
6452
+ }
6453
+ );
6454
+ }
6422
6455
  const transport = new StdioServerTransport();
6423
6456
  await server.connect(transport);
6424
6457
  }