@sideboard-ai/core 0.1.261 → 0.1.263

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 (55) hide show
  1. package/dist/{abletime-OQ4PC52L.js → abletime-6AOJI2YZ.js} +4 -4
  2. package/dist/{abletime-CDGINDXG.js → abletime-U75ZGDIX.js} +5 -5
  3. package/dist/{abletime-oauth-KX2TDFTE.js → abletime-oauth-LOAHGG4X.js} +3 -3
  4. package/dist/{abletime-oauth-5A5EOH2A.js → abletime-oauth-TA2YDCYX.js} +4 -4
  5. package/dist/{abletime-rest-UEHOBLXD.js → abletime-rest-CTA5WV26.js} +4 -4
  6. package/dist/{abletime-rest-PUGKYTRR.js → abletime-rest-YZPFM6IY.js} +3 -3
  7. package/dist/agents/claude-runner.cjs +463 -0
  8. package/dist/agents/claude-runner.d.cts +1 -0
  9. package/dist/agents/claude-runner.d.ts +1 -0
  10. package/dist/agents/claude-runner.js +134 -0
  11. package/dist/agents/cursor-runner.cjs +388 -7
  12. package/dist/agents/cursor-runner.js +34 -7
  13. package/dist/{agents-J2P4KNEY.js → agents-3TUMFSMH.js} +10 -9
  14. package/dist/{agents-IKVUGW5H.js → agents-6FWDRVA2.js} +4 -4
  15. package/dist/{app-settings-OM7H64YM.js → app-settings-MVZPM6WW.js} +1 -1
  16. package/dist/{app-settings-VQEUCONM.js → app-settings-RKFFUIFQ.js} +2 -2
  17. package/dist/{chunk-6PFZ6XO7.js → chunk-2QUQH2SZ.js} +81 -6
  18. package/dist/{chunk-VIC6TDYT.js → chunk-36FOBJBF.js} +76 -3
  19. package/dist/{chunk-ABPNIYRS.js → chunk-3H7O27X2.js} +389 -102
  20. package/dist/{chunk-FSZ7YYFQ.js → chunk-4ALDX5IO.js} +5 -3
  21. package/dist/{chunk-5OKJDCBK.js → chunk-5PK53SAP.js} +5 -5
  22. package/dist/{chunk-U3AOJLH3.js → chunk-5ZPT5YAV.js} +8 -6
  23. package/dist/{chunk-YFTO7U2A.js → chunk-6YOXQVJ7.js} +3 -3
  24. package/dist/{chunk-J6ONZP74.js → chunk-6YY7XPPL.js} +5 -5
  25. package/dist/{chunk-WV7KJH2E.js → chunk-A5SAYFJ4.js} +1 -0
  26. package/dist/{chunk-B6XFBNYM.js → chunk-BOMSJUXW.js} +2 -2
  27. package/dist/{chunk-S7VDDJ34.js → chunk-CEOTK5GZ.js} +3 -3
  28. package/dist/{chunk-4F7J6UWD.js → chunk-EK3KVT7I.js} +515 -139
  29. package/dist/{chunk-HHEKVR6C.js → chunk-F5MFNUAA.js} +40 -2
  30. package/dist/{chunk-KVQX62XI.js → chunk-K7B25HO4.js} +3 -3
  31. package/dist/{chunk-JTNY5CK4.js → chunk-L5WGAIL2.js} +46 -46
  32. package/dist/chunk-NYBFYFK7.js +415 -0
  33. package/dist/{chunk-GIZQAOHZ.js → chunk-QX6QUZVD.js} +121 -91
  34. package/dist/{chunk-X2OTZ36G.js → chunk-RKPBIB2U.js} +52 -52
  35. package/dist/{chunk-W5PWGEKS.js → chunk-T3AYRQJZ.js} +107 -77
  36. package/dist/{chunk-D554YCHG.js → chunk-YKYEHDMX.js} +2 -2
  37. package/dist/connected-teams-RU6VATCM.js +1 -1
  38. package/dist/{coordinator-prompt-VQHQ3RIX.js → coordinator-prompt-HZCQZ6C5.js} +6 -6
  39. package/dist/{coordinator-prompt-TU6656OK.js → coordinator-prompt-J7DKHOCH.js} +3 -3
  40. package/dist/{global-workspace-PHF2MOGW.js → global-workspace-6LDUZ2NF.js} +3 -3
  41. package/dist/{global-workspace-47NZVA44.js → global-workspace-TI26K25J.js} +6 -6
  42. package/dist/index.cjs +1712 -721
  43. package/dist/index.d.cts +181 -41
  44. package/dist/index.d.ts +181 -41
  45. package/dist/index.js +693 -285
  46. package/dist/mcp/run-stdio.cjs +10469 -9896
  47. package/dist/mcp/run-stdio.js +193 -168
  48. package/dist/{orchestrator-VLTO5JBR.js → orchestrator-4JSG7BWC.js} +5 -5
  49. package/dist/{orchestrator-OCG4QVA5.js → orchestrator-5YU46YNS.js} +11 -10
  50. package/dist/{workspaces-I7TWFTJM.js → workspaces-QJ3XIPUJ.js} +6 -6
  51. package/dist/{workspaces-2XTDS7N2.js → workspaces-WOQHJFRQ.js} +3 -3
  52. package/dist/{worktree-DDDF3WHV.js → worktree-6U2FA5LO.js} +2 -2
  53. package/dist/{worktree-YASFE65H.js → worktree-KQA4ON2G.js} +5 -5
  54. package/package.json +4 -3
  55. /package/dist/{chunk-LDXSAVWO.js → chunk-GWFVCCJZ.js} +0 -0
@@ -11,9 +11,9 @@ import {
11
11
  searchAbleTimeTasks,
12
12
  toAbleTimeIssueInfo,
13
13
  updateAbleTimeTask
14
- } from "../chunk-S7VDDJ34.js";
15
- import "../chunk-B6XFBNYM.js";
16
- import "../chunk-5OKJDCBK.js";
14
+ } from "../chunk-CEOTK5GZ.js";
15
+ import "../chunk-BOMSJUXW.js";
16
+ import "../chunk-5PK53SAP.js";
17
17
  import {
18
18
  AGENT_GIT_ACTIONS,
19
19
  BOARD_COLUMN_DEFS,
@@ -51,8 +51,9 @@ import {
51
51
  syncBoardPins,
52
52
  threadsSharingWorktree,
53
53
  updateSchedule,
54
- waitForDetachedJob
55
- } from "../chunk-W5PWGEKS.js";
54
+ waitForDetachedJob,
55
+ waitForTurnToolResult
56
+ } from "../chunk-T3AYRQJZ.js";
56
57
  import {
57
58
  PRESENT_PLAN_REQUIRES_PLAN_MODE,
58
59
  PRESENT_PLAN_TOOL_DESCRIPTION,
@@ -69,15 +70,17 @@ import {
69
70
  listModelsForAgent,
70
71
  looksLikeHugeToolResultDump,
71
72
  materializeSystemCaBundle,
73
+ shouldRegisterMcpWaitForTurn,
72
74
  sideboardMcpProfile,
73
- trustSystemCertificates
74
- } from "../chunk-4F7J6UWD.js";
75
+ trustSystemCertificates,
76
+ worktreeMcpToolNames
77
+ } from "../chunk-EK3KVT7I.js";
75
78
  import {
76
79
  GLOBAL_WORKSPACE_ID,
77
80
  isCloudCoordinatorThread,
78
81
  isOrchestratorThread,
79
82
  listWorkspaces
80
- } from "../chunk-JTNY5CK4.js";
83
+ } from "../chunk-L5WGAIL2.js";
81
84
  import {
82
85
  canonicalizeRepoPath,
83
86
  formatGhLandError,
@@ -88,7 +91,7 @@ import {
88
91
  resolveListPrsOptions,
89
92
  resolveRepoRoot,
90
93
  warmGithubAgentAuth
91
- } from "../chunk-FSZ7YYFQ.js";
94
+ } from "../chunk-4ALDX5IO.js";
92
95
  import {
93
96
  CLI_DIR
94
97
  } from "../chunk-N2WW62LF.js";
@@ -120,7 +123,7 @@ import {
120
123
  saveLinearOAuth,
121
124
  writeSecureJson,
122
125
  writeViewerContext
123
- } from "../chunk-VIC6TDYT.js";
126
+ } from "../chunk-36FOBJBF.js";
124
127
  import {
125
128
  writePrivateFile
126
129
  } from "../chunk-NSTQ6QKD.js";
@@ -229,7 +232,7 @@ function createClippedMcpServer() {
229
232
 
230
233
  // src/mcp/server.ts
231
234
  import { z as z9 } from "zod";
232
- import { randomUUID as randomUUID2 } from "crypto";
235
+ import { randomUUID as randomUUID3 } from "crypto";
233
236
  import { basename } from "path";
234
237
 
235
238
  // src/integrations/issue-since.ts
@@ -1935,10 +1938,10 @@ function formatAskUserNotifyMessage(questions) {
1935
1938
  }
1936
1939
  function formatNotifyOrchestratorPrompt(input) {
1937
1940
  const title = input.child.title?.trim() || "Untitled";
1938
- const link = `[${title}](sideboard://thread/${input.child.id})`;
1941
+ const link = `[${title}](sideboard://chat/${input.child.id})`;
1939
1942
  const body = clampMessage(input.message) || "(no details)";
1940
1943
  const state = input.reason === "input-required" ? "is input-required (waiting for the user in that chat)" : "is blocked";
1941
- const next = input.reason === "input-required" ? "Wait for the user in that chat. Do not send_to_thread a check-in." : "Help if this is your child; otherwise briefly tell the user. Do not send_to_thread a check-in.";
1944
+ const next = input.reason === "input-required" ? "Wait for the user in that chat. Do not send_to_chat a check-in." : "Help if this is your child; otherwise briefly tell the user. Do not send_to_chat a check-in.";
1942
1945
  return [
1943
1946
  `Sideboard: child worktree ${link} ${state}.`,
1944
1947
  body,
@@ -2457,7 +2460,7 @@ async function continueSourceThread(threadId, prompt) {
2457
2460
  await continueOnReply(threadId, prompt);
2458
2461
  return;
2459
2462
  }
2460
- const { getOrchestrator: getOrchestrator2 } = await import("../orchestrator-VLTO5JBR.js");
2463
+ const { getOrchestrator: getOrchestrator2 } = await import("../orchestrator-4JSG7BWC.js");
2461
2464
  await getOrchestrator2().send(threadId, prompt);
2462
2465
  } catch {
2463
2466
  }
@@ -3546,7 +3549,7 @@ function registerAbleTimeTools(server) {
3546
3549
  );
3547
3550
  server.tool(
3548
3551
  "abletime_ensure_task",
3549
- "Find or create an AbleTime task for this work, then create_thread with sourceType=ticket.",
3552
+ "Find or create an AbleTime task for this work, then create_workspace with sourceType=ticket.",
3550
3553
  {
3551
3554
  title: z4.string(),
3552
3555
  description: z4.string().optional(),
@@ -3870,11 +3873,11 @@ async function resolveProjectRepoPath(repoPath) {
3870
3873
  }
3871
3874
  return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
3872
3875
  }
3873
- var PROJECT_PATH_HINT = "Pass repoPath from list_workspaces. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
3876
+ var PROJECT_PATH_HINT = "Pass repoPath from list_projects. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
3874
3877
  function registerViewerContextTools(server) {
3875
3878
  server.tool(
3876
3879
  "get_viewer_context",
3877
- "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_workspaces for project context. Worktree turns may omit repoPath (uses cwd).",
3880
+ "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_projects for project context. Worktree turns may omit repoPath (uses cwd).",
3878
3881
  {
3879
3882
  repoPath: z7.string().optional().describe(
3880
3883
  "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
@@ -3900,7 +3903,7 @@ function registerViewerContextTools(server) {
3900
3903
  context: z7.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
3901
3904
  confirmed: z7.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
3902
3905
  repoPath: z7.string().optional().describe(
3903
- "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
3906
+ "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
3904
3907
  )
3905
3908
  },
3906
3909
  async ({ scope, context, confirmed, repoPath }) => {
@@ -4014,7 +4017,7 @@ function registerScheduleTools(server) {
4014
4017
  );
4015
4018
  server.tool(
4016
4019
  "create_schedule",
4017
- "Create a local schedule that, when due, sends a prompt to an existing chat (threadId) or starts a new Global orchestration chat (omit threadId). Pass threadId=self to continue this chat. If that chat is missing or archived when the job fires, Sideboard starts a new Global chat (recurring jobs then continue that chat). Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without threadId open a new Global chat each run. Creating or enabling a future job turns on Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled so the Mac stays awake until due. Sideboard.app must be running for the job to fire.",
4020
+ "Create a local schedule that, when due, sends a prompt to an existing chat (chatId) or starts a new Global orchestration chat (omit chatId). Pass chatId=self to continue this chat. If that chat is missing or archived when the job fires, Sideboard starts a new Global chat (recurring jobs then continue that chat). Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without chatId open a new Global chat each run. Creating or enabling a future job turns on Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled so the Mac stays awake until due. Sideboard.app must be running for the job to fire.",
4018
4021
  {
4019
4022
  prompt: z8.string().describe("User message / goal queued when the schedule fires"),
4020
4023
  name: z8.string().optional(),
@@ -4022,20 +4025,22 @@ function registerScheduleTools(server) {
4022
4025
  every: z8.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
4023
4026
  cron: z8.string().optional().describe("5-field cron expression"),
4024
4027
  tz: z8.string().optional().describe("IANA timezone for cron (default: system)"),
4025
- threadId: z8.string().optional().describe(
4026
- 'Existing chat id (orchestration or worktree), or "self" for this chat. Omit to create a new Global chat on fire.'
4028
+ chatId: z8.string().optional().describe(
4029
+ 'Existing chat id (orchestration or workspace), or "self" for this chat. Omit to create a new Global chat on fire.'
4027
4030
  ),
4031
+ threadId: z8.string().optional().describe("Legacy alias for chatId."),
4028
4032
  agent: z8.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
4029
4033
  model: z8.string().optional()
4030
4034
  },
4031
4035
  async (args) => {
4032
4036
  try {
4033
4037
  const when = parseWhen(args);
4034
- const threadId = resolveScheduleThreadId(args.threadId);
4035
- if (args.threadId?.trim().toLowerCase() === "self" && !threadId) {
4038
+ const rawChatId = args.chatId ?? args.threadId;
4039
+ const threadId = resolveScheduleThreadId(rawChatId);
4040
+ if (rawChatId?.trim().toLowerCase() === "self" && !threadId) {
4036
4041
  return fail5(
4037
4042
  new Error(
4038
- "threadId=self needs this chat id (SIDEBOARD_THREAD_ID or SIDEBOARD_ORCHESTRATOR_THREAD_ID)"
4043
+ "chatId=self needs this chat id (SIDEBOARD_THREAD_ID or SIDEBOARD_ORCHESTRATOR_THREAD_ID)"
4039
4044
  )
4040
4045
  );
4041
4046
  }
@@ -4050,7 +4055,7 @@ function registerScheduleTools(server) {
4050
4055
  });
4051
4056
  return text6({
4052
4057
  schedule,
4053
- hint: scheduleNeedsCaffeinate(schedule) ? "Fires while Sideboard.app is running. Recurring jobs without threadId create a new orchestration chat each run. Caffeinate while schedules are enabled is now on so the Mac can stay awake until this job is due." : "Fires while Sideboard.app is running. Recurring jobs without threadId create a new orchestration chat each run."
4058
+ hint: scheduleNeedsCaffeinate(schedule) ? "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run. Caffeinate while schedules are enabled is now on so the Mac can stay awake until this job is due." : "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run."
4054
4059
  });
4055
4060
  } catch (err) {
4056
4061
  return fail5(err);
@@ -4059,7 +4064,7 @@ function registerScheduleTools(server) {
4059
4064
  );
4060
4065
  server.tool(
4061
4066
  "update_schedule",
4062
- "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
4067
+ "Update a local schedule (prompt, cadence, target chat, enabled). Pass id from list_schedules.",
4063
4068
  {
4064
4069
  id: z8.string(),
4065
4070
  prompt: z8.string().optional(),
@@ -4068,9 +4073,10 @@ function registerScheduleTools(server) {
4068
4073
  every: z8.string().optional(),
4069
4074
  cron: z8.string().optional(),
4070
4075
  tz: z8.string().optional(),
4071
- threadId: z8.string().optional().describe(
4072
- 'Existing chat (orchestration or worktree), "self", or empty string to create a new Global chat each run'
4076
+ chatId: z8.string().optional().describe(
4077
+ 'Existing chat (orchestration or workspace), "self", or empty string to create a new Global chat each run'
4073
4078
  ),
4079
+ threadId: z8.string().optional().describe("Legacy alias for chatId."),
4074
4080
  agent: z8.enum(["claude", "cursor", "codex", "opencode"]).optional(),
4075
4081
  model: z8.string().optional(),
4076
4082
  enabled: z8.boolean().optional()
@@ -4085,12 +4091,13 @@ function registerScheduleTools(server) {
4085
4091
  tz: args.tz
4086
4092
  }) : void 0;
4087
4093
  let threadId;
4088
- if (args.threadId !== void 0) {
4089
- threadId = resolveScheduleThreadId(args.threadId);
4090
- if (args.threadId.trim().toLowerCase() === "self" && !threadId) {
4094
+ const rawChatId = args.chatId ?? args.threadId;
4095
+ if (rawChatId !== void 0) {
4096
+ threadId = resolveScheduleThreadId(rawChatId);
4097
+ if (rawChatId.trim().toLowerCase() === "self" && !threadId) {
4091
4098
  return fail5(
4092
4099
  new Error(
4093
- "threadId=self needs this chat id (SIDEBOARD_THREAD_ID or SIDEBOARD_ORCHESTRATOR_THREAD_ID)"
4100
+ "chatId=self needs this chat id (SIDEBOARD_THREAD_ID or SIDEBOARD_ORCHESTRATOR_THREAD_ID)"
4094
4101
  )
4095
4102
  );
4096
4103
  }
@@ -4141,12 +4148,29 @@ function registerScheduleTools(server) {
4141
4148
  );
4142
4149
  }
4143
4150
 
4151
+ // src/agent-rpc/handlers.ts
4152
+ import { randomUUID } from "crypto";
4153
+ import { isAbsolute } from "path";
4154
+ function presentArtifactResult(input) {
4155
+ const id = input.artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
4156
+ return {
4157
+ ok: true,
4158
+ artifact_id: id,
4159
+ title: input.title,
4160
+ type: input.type,
4161
+ status: input.status,
4162
+ phase: input.phase,
4163
+ mode: input.type === "log" ? input.mode ?? "append" : void 0,
4164
+ message: input.type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
4165
+ };
4166
+ }
4167
+
4144
4168
  // src/board/load-home-board.ts
4145
4169
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
4146
4170
  import { join as join6 } from "path";
4147
4171
 
4148
4172
  // src/board/board-pins.ts
4149
- import { randomUUID } from "crypto";
4173
+ import { randomUUID as randomUUID2 } from "crypto";
4150
4174
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
4151
4175
  import { join as join5 } from "path";
4152
4176
  var FILE = "home-board-pins.json";
@@ -4467,7 +4491,7 @@ function withTimeout(promise, ms, label) {
4467
4491
  }
4468
4492
  async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
4469
4493
  const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
4470
- let parentId = args.parentThreadId?.trim() || "";
4494
+ let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
4471
4495
  let parent = parentId ? orch.getThread(parentId) : null;
4472
4496
  let parentCorrectedFrom;
4473
4497
  if (envParentId) {
@@ -4517,7 +4541,7 @@ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
4517
4541
  attachments: args.attachments
4518
4542
  }),
4519
4543
  CREATE_THREAD_TIMEOUT_MS,
4520
- "create_thread"
4544
+ "create_workspace"
4521
4545
  );
4522
4546
  const alreadyStarted = priorIds.has(thread.id);
4523
4547
  const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
@@ -4532,7 +4556,8 @@ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
4532
4556
  model: thread.model,
4533
4557
  status: thread.status,
4534
4558
  cowboy: Boolean(thread.cowboy),
4535
- link: `sideboard://thread/${thread.id}`,
4559
+ link: `sideboard://chat/${thread.id}`,
4560
+ parentChatId: thread.parentThreadId,
4536
4561
  parentThreadId: thread.parentThreadId,
4537
4562
  ...alreadyStarted ? { alreadyStarted: true } : {},
4538
4563
  ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
@@ -4543,7 +4568,7 @@ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
4543
4568
  };
4544
4569
  } catch (err) {
4545
4570
  const message = err instanceof Error ? err.message : String(err);
4546
- return { ok: false, text: `create_thread failed: ${message}` };
4571
+ return { ok: false, text: `create_workspace failed: ${message}` };
4547
4572
  }
4548
4573
  }
4549
4574
  function previewHint(preview) {
@@ -4560,7 +4585,7 @@ async function startMcpServer() {
4560
4585
  );
4561
4586
  }
4562
4587
  try {
4563
- const { maxConcurrentAgents } = await import("../app-settings-OM7H64YM.js");
4588
+ const { maxConcurrentAgents } = await import("../app-settings-MVZPM6WW.js");
4564
4589
  orch.setMaxConcurrent(maxConcurrentAgents());
4565
4590
  } catch {
4566
4591
  }
@@ -4593,8 +4618,8 @@ async function startMcpServer() {
4593
4618
  }
4594
4619
  if (!worktreeProfile) {
4595
4620
  server.tool(
4596
- "list_workspaces",
4597
- "List registered Sideboard workspaces (repos). Each line is name, path, github:owner/repo when resolvable, and project context when set \u2014 use path as repoPath for list_board/list_branches/list_prs/list_issues/create_thread. Account / project context (Settings \u2192 Agents / Projects) says which tickets and review PRs belong to the user.",
4621
+ "list_projects",
4622
+ "List registered projects (git repos). Each line is name, path, github:owner/repo when resolvable, and project context when set \u2014 use path as repoPath for list_board/list_branches/list_prs/list_issues/create_workspace. Account / project context (Settings \u2192 Agents / Projects) says which tickets and review PRs belong to the user.",
4598
4623
  {},
4599
4624
  async () => {
4600
4625
  const workspaces = orch.listWorkspaces();
@@ -4613,14 +4638,14 @@ async function startMcpServer() {
4613
4638
  );
4614
4639
  return {
4615
4640
  content: [
4616
- { type: "text", text: lines.join("\n") || "(no workspaces)" }
4641
+ { type: "text", text: lines.join("\n") || "(no projects)" }
4617
4642
  ]
4618
4643
  };
4619
4644
  }
4620
4645
  );
4621
4646
  server.tool(
4622
- "list_threads",
4623
- "List Sideboard threads across all workspaces (one summary line each \u2014 token-frugal). Includes parent id, last message preview, and live progress so you can see worktree children. Each line ends with sideboard://thread/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
4647
+ "list_chats",
4648
+ "List chats across all projects (one summary line each \u2014 token-frugal). Includes parent id, last message preview, and live progress so you can see workspace children. Each line ends with sideboard://chat/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
4624
4649
  {},
4625
4650
  async () => {
4626
4651
  const threads = orch.getThreads(true);
@@ -4632,19 +4657,19 @@ async function startMcpServer() {
4632
4657
  const previewBit = preview ? ` ${preview}` : "";
4633
4658
  const err = t.lastError ? ` error:${t.lastError.replace(/\s+/g, " ").slice(0, 60)}` : "";
4634
4659
  const progress = live?.summary && !isInternalAgentStatusText(live.summary) ? ` ${live.summary}` : "";
4635
- return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${err} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
4660
+ return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${err} sideboard://chat/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
4636
4661
  });
4637
4662
  return {
4638
- content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
4663
+ content: [{ type: "text", text: lines.join("\n") || "(no chats)" }]
4639
4664
  };
4640
4665
  }
4641
4666
  );
4642
4667
  server.tool(
4643
4668
  "list_board",
4644
- "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_thread adds a worktree (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
4669
+ "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
4645
4670
  {
4646
4671
  query: z9.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
4647
- repoPath: z9.string().optional().describe("Limit to one workspace path from list_workspaces"),
4672
+ repoPath: z9.string().optional().describe("Limit to one project path from list_projects"),
4648
4673
  kind: z9.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
4649
4674
  ownership: z9.enum(["all", "mine", "reviewing"]).optional().describe(
4650
4675
  "Mine = PRs you authored or local WIP; reviewing = someone else's PR. Uses the gh login."
@@ -4680,13 +4705,13 @@ async function startMcpServer() {
4680
4705
  }
4681
4706
  );
4682
4707
  server.tool(
4683
- "get_thread",
4684
- "Get a compact thread summary by id/ref. Includes last message preview, parentThreadId, and child worktree threads (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
4708
+ "get_chat",
4709
+ "Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
4685
4710
  { ref: z9.string() },
4686
4711
  async ({ ref }) => {
4687
4712
  const t = orch.getThread(ref);
4688
4713
  if (!t) {
4689
- return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
4714
+ return { content: [{ type: "text", text: `Chat not found: ${ref}` }], isError: true };
4690
4715
  }
4691
4716
  const stillRunning = orch.threadLooksLive(t);
4692
4717
  const live = stillRunning ? readTurnLive(t.id) : null;
@@ -4702,6 +4727,7 @@ async function startMcpServer() {
4702
4727
  branchName: t.branchName,
4703
4728
  worktreePath: t.worktreePath,
4704
4729
  sessionId: t.sessionId,
4730
+ parentChatId: t.parentThreadId,
4705
4731
  parentThreadId: t.parentThreadId,
4706
4732
  children: childThreadRefs(t.id, orch.getThreads(false)),
4707
4733
  queueLength: t.queue.length,
@@ -4720,37 +4746,31 @@ async function startMcpServer() {
4720
4746
  }
4721
4747
  );
4722
4748
  }
4723
- server.tool(
4724
- "present_artifact",
4725
- "Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
4726
- {
4727
- title: z9.string().describe("Short title shown in the artifact pane header"),
4728
- type: z9.enum(["html", "svg", "markdown", "react", "log"]).describe(
4729
- "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
4730
- ),
4731
- content: z9.string().describe(
4732
- "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
4733
- ),
4734
- artifact_id: z9.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
4735
- status: z9.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
4736
- phase: z9.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
4737
- mode: z9.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
4738
- },
4739
- async ({ title, type, artifact_id, status, phase, mode }) => {
4740
- const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
4741
- const payload = {
4742
- ok: true,
4743
- artifact_id: id,
4744
- title,
4745
- type,
4746
- status,
4747
- phase,
4748
- mode: type === "log" ? mode ?? "append" : void 0,
4749
- message: type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
4750
- };
4751
- return mcpJson(payload);
4752
- }
4753
- );
4749
+ const mcpCatalog = new Set(worktreeMcpToolNames());
4750
+ if (mcpCatalog.has("present_artifact")) {
4751
+ server.tool(
4752
+ "present_artifact",
4753
+ "Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
4754
+ {
4755
+ title: z9.string().describe("Short title shown in the artifact pane header"),
4756
+ type: z9.enum(["html", "svg", "markdown", "react", "log"]).describe(
4757
+ "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
4758
+ ),
4759
+ content: z9.string().describe(
4760
+ "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
4761
+ ),
4762
+ artifact_id: z9.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
4763
+ status: z9.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
4764
+ phase: z9.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
4765
+ mode: z9.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
4766
+ },
4767
+ async ({ title, type, artifact_id, status, phase, mode }) => {
4768
+ return mcpJson(
4769
+ presentArtifactResult({ title, type, artifact_id, status, phase, mode })
4770
+ );
4771
+ }
4772
+ );
4773
+ }
4754
4774
  server.tool(
4755
4775
  "ask_user",
4756
4776
  "Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
@@ -4867,32 +4887,34 @@ async function startMcpServer() {
4867
4887
  return mcpJson(payload);
4868
4888
  }
4869
4889
  );
4870
- server.tool(
4871
- "wait_for_job",
4872
- "Wait on a detached long job (tests, pack, deploy, connector CLI) started with detached-job.cjs. MCP clients kill tools around 60s, so this returns within 45s. Sideboard opens a type=log pane from this result (artifact_id = job id, content = delta). stillRunning is the source of truth \u2014 if true, call wait_for_job again. If the job is hanging, producing no useful output, or doing the wrong thing, call stop_job instead of looping forever. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
4873
- {
4874
- id: z9.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4875
- timeoutMs: z9.number().optional()
4876
- },
4877
- async ({ id, timeoutMs }) => {
4878
- const result = await waitForDetachedJob(process.cwd(), id, {
4879
- timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
4880
- });
4881
- return mcpJson(result);
4882
- }
4883
- );
4884
- server.tool(
4885
- "stop_job",
4886
- "Stop a detached job you started with detached-job.cjs when it is hanging, producing no useful output, buffering forever, or doing the wrong thing (wrong project, infinite watch, huge dump). Do not stop a pack/test/deploy that is clearly making progress. The type=log pane updates to status=failed with the last delta. Then decide the next step.",
4887
- {
4888
- id: z9.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4889
- reason: z9.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
4890
- },
4891
- async ({ id, reason }) => {
4892
- const result = await stopDetachedJob(process.cwd(), id, { reason });
4893
- return mcpJson(result);
4894
- }
4895
- );
4890
+ if (mcpCatalog.has("wait_for_job")) {
4891
+ server.tool(
4892
+ "wait_for_job",
4893
+ "Wait on a detached long job (tests, pack, deploy, connector CLI) started with detached-job.cjs. MCP clients kill tools around 60s, so this returns within 45s. Sideboard opens a type=log pane from this result (artifact_id = job id, content = delta). stillRunning is the source of truth \u2014 if true, call wait_for_job again. If the job is hanging, producing no useful output, or doing the wrong thing, call stop_job instead of looping forever. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
4894
+ {
4895
+ id: z9.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4896
+ timeoutMs: z9.number().optional()
4897
+ },
4898
+ async ({ id, timeoutMs }) => {
4899
+ const result = await waitForDetachedJob(process.cwd(), id, {
4900
+ timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
4901
+ });
4902
+ return mcpJson(result);
4903
+ }
4904
+ );
4905
+ server.tool(
4906
+ "stop_job",
4907
+ "Stop a detached job you started with detached-job.cjs when it is hanging, producing no useful output, buffering forever, or doing the wrong thing (wrong project, infinite watch, huge dump). Do not stop a pack/test/deploy that is clearly making progress. The type=log pane updates to status=failed with the last delta. Then decide the next step.",
4908
+ {
4909
+ id: z9.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
4910
+ reason: z9.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
4911
+ },
4912
+ async ({ id, reason }) => {
4913
+ const result = await stopDetachedJob(process.cwd(), id, { reason });
4914
+ return mcpJson(result);
4915
+ }
4916
+ );
4917
+ }
4896
4918
  server.tool(
4897
4919
  "list_run_scripts",
4898
4920
  "List .sideboard/.conductor run scripts for this thread (same menu as the desktop Dev button) and which are active. Each script and active run includes preview: url (open http://localhost:<SIDEBOARD_PORT>) or window (Electron: native window is the app; url is renderer/HMR only). Never guess :3000. Worktree turns may omit ref (uses cwd).",
@@ -5052,7 +5074,7 @@ async function startMcpServer() {
5052
5074
  registerSlackTools(server);
5053
5075
  registerConnectedIssueVendorTools(server);
5054
5076
  const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-UAGLSGMM.js");
5055
- const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-OM7H64YM.js");
5077
+ const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-MVZPM6WW.js");
5056
5078
  const accountDefaults = resolveThreadDefaults();
5057
5079
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
5058
5080
  server.tool(
@@ -5082,8 +5104,8 @@ async function startMcpServer() {
5082
5104
  }
5083
5105
  );
5084
5106
  server.tool(
5085
- "create_thread",
5086
- `Create a worktree thread (chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). 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. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
5107
+ "create_workspace",
5108
+ `Create a workspace (isolated git worktree + first chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_chat to chat.`,
5087
5109
  {
5088
5110
  sourceType: z9.enum(["branch", "pr", "ticket"]),
5089
5111
  sourceRef: z9.string(),
@@ -5098,9 +5120,10 @@ async function startMcpServer() {
5098
5120
  cowboy: z9.boolean().optional().describe(
5099
5121
  "If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
5100
5122
  ),
5101
- parentThreadId: z9.string().optional().describe(
5123
+ parentChatId: z9.string().optional().describe(
5102
5124
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5103
- )
5125
+ ),
5126
+ parentThreadId: z9.string().optional().describe("Legacy alias for parentChatId.")
5104
5127
  },
5105
5128
  async (args) => {
5106
5129
  const result = await createOrchChildThread(orch, args, resolveNewThreadOptions);
@@ -5112,11 +5135,11 @@ async function startMcpServer() {
5112
5135
  );
5113
5136
  server.tool(
5114
5137
  "start_board_card",
5115
- "Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_thread.",
5138
+ "Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_chat.",
5116
5139
  {
5117
5140
  kind: z9.enum(["ticket", "pr", "branch"]),
5118
5141
  ref: z9.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
5119
- repoPath: z9.string().describe("Workspace path from list_workspaces / list_board"),
5142
+ repoPath: z9.string().describe("Project path from list_projects / list_board"),
5120
5143
  title: z9.string().optional()
5121
5144
  },
5122
5145
  async ({ kind, ref, repoPath, title }) => {
@@ -5139,7 +5162,7 @@ async function startMcpServer() {
5139
5162
  id: existing.id,
5140
5163
  title: existing.title,
5141
5164
  status: existing.status,
5142
- link: `sideboard://thread/${existing.id}`
5165
+ link: `sideboard://chat/${existing.id}`
5143
5166
  });
5144
5167
  }
5145
5168
  const workspaces = orch.listWorkspaces();
@@ -5167,7 +5190,7 @@ async function startMcpServer() {
5167
5190
  const ident = issue?.identifier ?? ref.trim();
5168
5191
  const attachments = issue ? [
5169
5192
  {
5170
- id: randomUUID2(),
5193
+ id: randomUUID3(),
5171
5194
  name: issue.identifier,
5172
5195
  kind: "issue",
5173
5196
  content: [
@@ -5211,8 +5234,8 @@ async function startMcpServer() {
5211
5234
  }
5212
5235
  );
5213
5236
  server.tool(
5214
- "send_to_thread",
5215
- 'Steer a prompt on a worktree thread chat (Settings \u2192 Follow-up, default steer: front of the queue, skips the inbox; while the desktop board is running it starts as soon as the in-flight turn yields). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not send_to_thread to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
5237
+ "send_to_chat",
5238
+ 'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer: front of the queue, skips the inbox; while the desktop board is running it starts as soon as the in-flight turn yields). Use after create_workspace to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not send_to_chat to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
5216
5239
  {
5217
5240
  ref: z9.string(),
5218
5241
  prompt: z9.string(),
@@ -5236,36 +5259,38 @@ async function startMcpServer() {
5236
5259
  });
5237
5260
  }
5238
5261
  );
5239
- server.tool(
5240
- "wait_for_turn",
5241
- "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. If stillRunning, call wait_for_turn again \u2014 do not send_to_thread a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_thread or tell the user. On input-required, wait for the user in that chat. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
5242
- {
5243
- ref: z9.string(),
5244
- timeoutMs: z9.number().optional()
5245
- },
5246
- async ({ ref, timeoutMs }) => {
5247
- const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
5248
- resolveIfStillRunning: true
5249
- });
5250
- const result = orch.getTurnResult(thread.id);
5251
- const hint = mcpWaitTaskHint(result.taskState, result.status);
5252
- return mcpJson({
5253
- id: thread.id,
5254
- status: result.status,
5255
- taskState: result.taskState,
5256
- text: result.text,
5257
- lastError: result.lastError,
5258
- stillRunning: result.stillRunning,
5259
- progress: result.progress,
5260
- lastActivityAt: result.lastActivityAt,
5261
- hint,
5262
- incomplete: needsCoordinatorAction(result.taskState)
5263
- });
5264
- }
5265
- );
5262
+ if (shouldRegisterMcpWaitForTurn()) {
5263
+ server.tool(
5264
+ "wait_for_turn",
5265
+ "Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
5266
+ {
5267
+ ref: z9.string(),
5268
+ timeoutMs: z9.number().optional()
5269
+ },
5270
+ async ({ ref, timeoutMs }) => {
5271
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
5272
+ resolveIfStillRunning: true
5273
+ });
5274
+ const result = orch.getTurnResult(thread.id);
5275
+ return mcpJson(
5276
+ waitForTurnToolResult({
5277
+ id: thread.id,
5278
+ status: result.status,
5279
+ taskState: result.taskState,
5280
+ text: result.text,
5281
+ lastError: result.lastError,
5282
+ stillRunning: result.stillRunning,
5283
+ progress: result.progress,
5284
+ lastActivityAt: result.lastActivityAt,
5285
+ usage: result.usage
5286
+ })
5287
+ );
5288
+ }
5289
+ );
5290
+ }
5266
5291
  server.tool(
5267
5292
  "get_turn_result",
5268
- "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript. Includes taskState (A2A lifecycle) and usage for the last agent turn (tokens + costUsd when reported).",
5293
+ "Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle).",
5269
5294
  { ref: z9.string() },
5270
5295
  async ({ ref }) => {
5271
5296
  const result = orch.getTurnResult(ref);
@@ -5278,7 +5303,7 @@ async function startMcpServer() {
5278
5303
  }
5279
5304
  );
5280
5305
  server.tool(
5281
- "stop_thread",
5306
+ "stop_chat",
5282
5307
  "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.",
5283
5308
  {
5284
5309
  ref: z9.string(),
@@ -5288,7 +5313,7 @@ async function startMcpServer() {
5288
5313
  const t = orch.getThread(ref);
5289
5314
  if (!t) {
5290
5315
  return {
5291
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
5316
+ content: [{ type: "text", text: `Chat not found: ${ref}` }],
5292
5317
  isError: true
5293
5318
  };
5294
5319
  }
@@ -5303,14 +5328,14 @@ async function startMcpServer() {
5303
5328
  }
5304
5329
  );
5305
5330
  server.tool(
5306
- "archive_thread",
5331
+ "archive_chat",
5307
5332
  "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
5308
5333
  { ref: z9.string() },
5309
5334
  async ({ ref }) => {
5310
5335
  const t = orch.getThread(ref);
5311
5336
  if (!t) {
5312
5337
  return {
5313
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
5338
+ content: [{ type: "text", text: `Chat not found: ${ref}` }],
5314
5339
  isError: true
5315
5340
  };
5316
5341
  }
@@ -5329,7 +5354,7 @@ async function startMcpServer() {
5329
5354
  }
5330
5355
  );
5331
5356
  server.tool(
5332
- "restore_thread",
5357
+ "restore_chat",
5333
5358
  "Restore an archived thread (recreates worktree from branch when needed)",
5334
5359
  { ref: z9.string() },
5335
5360
  async ({ ref }) => {
@@ -5388,7 +5413,7 @@ async function startMcpServer() {
5388
5413
  title: tab.title,
5389
5414
  status: tab.status,
5390
5415
  fromThreadId: from?.id ?? ref,
5391
- link: `sideboard://thread/${tab.id}`
5416
+ link: `sideboard://chat/${tab.id}`
5392
5417
  });
5393
5418
  } catch (err) {
5394
5419
  const message = err instanceof Error ? err.message : String(err);
@@ -5413,7 +5438,7 @@ async function startMcpServer() {
5413
5438
  status: thread.status,
5414
5439
  queueLength: thread.queue.length,
5415
5440
  action,
5416
- link: `sideboard://thread/${thread.id}`
5441
+ link: `sideboard://chat/${thread.id}`
5417
5442
  });
5418
5443
  } catch (err) {
5419
5444
  const raw = err instanceof Error ? err.message : String(err);
@@ -5423,7 +5448,7 @@ async function startMcpServer() {
5423
5448
  error: true,
5424
5449
  action,
5425
5450
  lastError: message,
5426
- hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_thread so the worktree agent runs gh pr create --draft --assignee @me -R <origin> --body-file <short.md>. Keep the description short." : void 0
5451
+ hint: /body is too long/i.test(message) ? "Branch is already pushed. send_to_chat so the worktree agent runs gh pr create --draft --assignee @me -R <origin> --body-file <short.md>. Keep the description short." : void 0
5427
5452
  },
5428
5453
  true
5429
5454
  );
@@ -5448,8 +5473,8 @@ async function startMcpServer() {
5448
5473
  }
5449
5474
  );
5450
5475
  server.tool(
5451
- "fork_worktree",
5452
- "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 (loop while stillRunning) on the returned id.",
5476
+ "fork_workspace",
5477
+ "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_chat / wait_for_turn (loop while stillRunning) on the returned id.",
5453
5478
  {
5454
5479
  ref: z9.string().describe("Worktree thread id/ref to fork"),
5455
5480
  through_index: z9.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -5477,7 +5502,7 @@ async function startMcpServer() {
5477
5502
  branchName: thread.branchName,
5478
5503
  worktreePath: thread.worktreePath,
5479
5504
  fromThreadId: source?.id ?? ref,
5480
- link: `sideboard://thread/${thread.id}`
5505
+ link: `sideboard://chat/${thread.id}`
5481
5506
  });
5482
5507
  } catch (err) {
5483
5508
  const message = err instanceof Error ? err.message : String(err);
@@ -5487,7 +5512,7 @@ async function startMcpServer() {
5487
5512
  );
5488
5513
  server.tool(
5489
5514
  "fork_chat",
5490
- "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). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
5515
+ "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). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only for worktree agents that need a new git worktree.",
5491
5516
  {
5492
5517
  ref: z9.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
5493
5518
  through_index: z9.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -5500,7 +5525,7 @@ async function startMcpServer() {
5500
5525
  const source = orch.getThread(ref);
5501
5526
  if (!source) {
5502
5527
  return {
5503
- content: [{ type: "text", text: `Thread not found: ${ref}` }],
5528
+ content: [{ type: "text", text: `Chat not found: ${ref}` }],
5504
5529
  isError: true
5505
5530
  };
5506
5531
  }
@@ -5520,7 +5545,7 @@ async function startMcpServer() {
5520
5545
  sourceType: tab.sourceType,
5521
5546
  worktreePath: tab.worktreePath,
5522
5547
  fromThreadId: source.id,
5523
- link: `sideboard://thread/${tab.id}`
5548
+ link: `sideboard://chat/${tab.id}`
5524
5549
  });
5525
5550
  } catch (err) {
5526
5551
  const message = err instanceof Error ? err.message : String(err);
@@ -5538,7 +5563,7 @@ async function startMcpServer() {
5538
5563
  }
5539
5564
  );
5540
5565
  server.tool(
5541
- "add_workspace",
5566
+ "add_project",
5542
5567
  "Register a git repo as a Sideboard workspace",
5543
5568
  { repoPath: z9.string() },
5544
5569
  async ({ repoPath }) => {
@@ -5547,7 +5572,7 @@ async function startMcpServer() {
5547
5572
  }
5548
5573
  );
5549
5574
  server.tool(
5550
- "remove_workspace",
5575
+ "remove_project",
5551
5576
  "Unregister a Sideboard workspace (does not archive threads)",
5552
5577
  { repoPath: z9.string() },
5553
5578
  async ({ repoPath }) => {
@@ -5580,7 +5605,7 @@ async function startMcpServer() {
5580
5605
  );
5581
5606
  server.tool(
5582
5607
  "list_branches",
5583
- "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).",
5608
+ "List git branches in a registered workspace. Pass repoPath from list_projects (unmerged into the default branch by default \u2014 for create_workspace sourceType=branch).",
5584
5609
  {
5585
5610
  repoPath: z9.string(),
5586
5611
  unmergedOnly: z9.boolean().optional()
@@ -5602,7 +5627,7 @@ async function startMcpServer() {
5602
5627
  );
5603
5628
  server.tool(
5604
5629
  "list_prs",
5605
- `List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_workspaces. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled with this project's ready-for-review GitHub label (Settings \u2192 Projects; default eng-review) with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_thread with sourceType=pr.`,
5630
+ `List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_projects. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled with this project's ready-for-review GitHub label (Settings \u2192 Projects; default eng-review) with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_workspace with sourceType=pr.`,
5606
5631
  {
5607
5632
  repoPath: z9.string(),
5608
5633
  query: z9.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
@@ -5674,7 +5699,7 @@ async function startMcpServer() {
5674
5699
  stackLayer: t.stackLayer,
5675
5700
  worktreePath: t.worktreePath,
5676
5701
  prUrl: t.prUrl,
5677
- link: `sideboard://thread/${t.id}`
5702
+ link: `sideboard://chat/${t.id}`
5678
5703
  }))
5679
5704
  });
5680
5705
  }
@@ -5695,7 +5720,7 @@ async function startMcpServer() {
5695
5720
  branchName: result.thread.branchName,
5696
5721
  stackLayer: result.thread.stackLayer,
5697
5722
  worktreePath: result.thread.worktreePath,
5698
- link: `sideboard://thread/${result.thread.id}`
5723
+ link: `sideboard://chat/${result.thread.id}`
5699
5724
  });
5700
5725
  }
5701
5726
  );
@@ -5726,14 +5751,14 @@ async function startMcpServer() {
5726
5751
  branchName: t.branchName,
5727
5752
  stackLayer: t.stackLayer,
5728
5753
  worktreePath: t.worktreePath,
5729
- link: `sideboard://thread/${t.id}`
5754
+ link: `sideboard://chat/${t.id}`
5730
5755
  }))
5731
5756
  });
5732
5757
  }
5733
5758
  );
5734
5759
  server.tool(
5735
5760
  "list_issues",
5736
- "List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Pass updatedSince for new/updated tickets and new comments (\u201Cany updates since yesterday?\u201D). Then create_thread with sourceType=ticket.",
5761
+ "List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Pass updatedSince for new/updated tickets and new comments (\u201Cany updates since yesterday?\u201D). Then create_workspace with sourceType=ticket.",
5737
5762
  {
5738
5763
  repoPath: z9.string(),
5739
5764
  query: z9.string().optional().describe("Search title, identifier, or description"),