@sideboard-ai/core 0.1.280 → 0.1.281

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 (52) hide show
  1. package/dist/{abletime-VCVPSLSU.js → abletime-WLDBJJUI.js} +3 -3
  2. package/dist/{abletime-VRATUMDO.js → abletime-X64E3GUJ.js} +3 -3
  3. package/dist/{abletime-oauth-4K7MICAD.js → abletime-oauth-AXR676W5.js} +2 -2
  4. package/dist/{abletime-oauth-4C4G2J3R.js → abletime-oauth-UWOCEICP.js} +2 -2
  5. package/dist/{abletime-rest-PIBCNHF3.js → abletime-rest-LPUDJUEP.js} +2 -2
  6. package/dist/{abletime-rest-6N63ZVQ3.js → abletime-rest-S4XWEXV2.js} +2 -2
  7. package/dist/{agents-DYCFLDNL.js → agents-2IE4HKHA.js} +5 -5
  8. package/dist/{agents-RXP5NZBC.js → agents-XA2E64AW.js} +5 -5
  9. package/dist/{chunk-DK4DKQ2H.js → chunk-2KNO2GJQ.js} +722 -139
  10. package/dist/{chunk-TZ5JHIYK.js → chunk-2SJXQ4UX.js} +68 -21
  11. package/dist/{chunk-K7MWJLR7.js → chunk-5MGLNVNK.js} +6 -906
  12. package/dist/{chunk-RK5PIWCW.js → chunk-ADXVTKLC.js} +12 -4
  13. package/dist/{chunk-Y7JPL4WA.js → chunk-BDAUPNHL.js} +3 -4
  14. package/dist/{chunk-R7HLXD4I.js → chunk-BS3QLVPL.js} +93 -57
  15. package/dist/{chunk-2P75E243.js → chunk-BYZ7F2BQ.js} +707 -128
  16. package/dist/{chunk-2J34G3WJ.js → chunk-DGPROCE5.js} +93 -57
  17. package/dist/{chunk-HQXBBUQX.js → chunk-DQZPF6WL.js} +12 -11
  18. package/dist/{chunk-YDXNCBA2.js → chunk-EIEAR62M.js} +12 -11
  19. package/dist/{chunk-XKZAVGTT.js → chunk-I6HSGKMZ.js} +1 -1
  20. package/dist/chunk-JPH7PYSK.js +1241 -0
  21. package/dist/{chunk-IPPY3FG5.js → chunk-K5XSTHHP.js} +6 -906
  22. package/dist/chunk-N5UGX3JL.js +1239 -0
  23. package/dist/{chunk-HNNS25SG.js → chunk-OCUIZD4Y.js} +14 -4
  24. package/dist/{chunk-IJDQKC5A.js → chunk-PY4UCMRI.js} +4 -4
  25. package/dist/{chunk-NQ7JMVSU.js → chunk-U7HAVAGJ.js} +1 -1
  26. package/dist/{chunk-PZURG3XN.js → chunk-VA55JG2I.js} +4 -4
  27. package/dist/{chunk-CPA43FBK.js → chunk-W7HIK2A6.js} +3 -4
  28. package/dist/{chunk-SKSOCIJT.js → chunk-WY2IAB7J.js} +68 -21
  29. package/dist/{chunk-QINJKNLR.js → chunk-XTPQEZZA.js} +2 -2
  30. package/dist/{conductor-J6WDOXWM.js → conductor-CNSCOC54.js} +5 -5
  31. package/dist/{conductor-OW6NAK3N.js → conductor-U776JSXQ.js} +5 -5
  32. package/dist/{coordinator-prompt-Z2D32HWK.js → coordinator-prompt-4DJIHSE6.js} +3 -3
  33. package/dist/{coordinator-prompt-ERTHREGJ.js → coordinator-prompt-SVVGU6AZ.js} +3 -3
  34. package/dist/{global-workspace-5PJXO45Z.js → global-workspace-UPUDWIS6.js} +3 -3
  35. package/dist/{global-workspace-AK5PSPQ2.js → global-workspace-XON4YZXE.js} +3 -3
  36. package/dist/index.cjs +3690 -2912
  37. package/dist/index.d.cts +101 -28
  38. package/dist/index.d.ts +101 -28
  39. package/dist/index.js +1013 -1001
  40. package/dist/mcp/run-stdio.cjs +1975 -1259
  41. package/dist/mcp/run-stdio.js +743 -785
  42. package/dist/{orchestrator-XEKODYDQ.js → orchestrator-FPGVKEID.js} +7 -7
  43. package/dist/{orchestrator-K66OMTQO.js → orchestrator-HWJHK5NL.js} +7 -7
  44. package/dist/{thread-store-B3ZM4QEZ.js → thread-store-7XBOUQ5K.js} +1 -1
  45. package/dist/{thread-store-SHCUBWB3.js → thread-store-ZQZDREVH.js} +1 -1
  46. package/dist/{workspaces-UP6DQ3WL.js → workspaces-7AJ5RZ3P.js} +7 -3
  47. package/dist/{workspaces-WU2RNLDV.js → workspaces-F76BWCSI.js} +7 -3
  48. package/dist/{worktree-L57T3KH7.js → worktree-NKLNVNXX.js} +20 -15
  49. package/dist/{worktree-Q6UJHG25.js → worktree-YTZC3UQN.js} +14 -9
  50. package/package.json +1 -1
  51. package/dist/chunk-FWNL62VA.js +0 -308
  52. package/dist/chunk-JANCPF7Z.js +0 -310
package/dist/index.js CHANGED
@@ -22,14 +22,17 @@ import {
22
22
  agentGitPrompt,
23
23
  alignQueueAttachments,
24
24
  appendQueuedItem,
25
+ applyIssueListWindow,
25
26
  applyPromptCacheTtlEnv,
26
27
  applyThreadIntoMain,
28
+ applyWorkspaceTags,
27
29
  armSchedules,
28
30
  assembleHomeBoard,
29
31
  boardPinIdentity,
30
32
  captureTurnBaseline,
31
33
  chatBlockedSuffix,
32
34
  claimDesktopHost,
35
+ clampMcpIssueLimit,
33
36
  classifyWorktreeColumn,
34
37
  classifyWorktreeListBadges,
35
38
  cleanupArchivedHistory,
@@ -68,11 +71,13 @@ import {
68
71
  formatAskUserNotifyMessage,
69
72
  formatDetachedJobInvoke,
70
73
  formatDevAccessHint,
74
+ formatGitHubSearchUpdatedSince,
71
75
  formatInjectedNoticesForTurn,
72
76
  formatIssueToolsDirective,
73
77
  formatIssueToolsReminder,
74
78
  formatLinearDirective,
75
79
  formatLinearReminder,
80
+ formatListedIssuesForMcp,
76
81
  formatLongRunningDirective,
77
82
  formatLongRunningReminder,
78
83
  formatNotifyOrchestratorPrompt,
@@ -112,6 +117,7 @@ import {
112
117
  isStaleLastErrorDuringTurn,
113
118
  isThisProcessDesktopHost,
114
119
  isUnclaimedRunScriptRequest,
120
+ issueMatchesUpdatedSince,
115
121
  issueNeedsWorkspacePick,
116
122
  issueTicketFromThread,
117
123
  lastAgentReply,
@@ -124,6 +130,8 @@ import {
124
130
  listWorktreeFiles,
125
131
  localhostPreviewUrl,
126
132
  markdownPreviewSource,
133
+ mcpIssueUpdatedSinceSchema,
134
+ mcpJson,
127
135
  mentionsPrGoal,
128
136
  mergeSetupOutput,
129
137
  moveQueuedItemToFront,
@@ -135,16 +143,20 @@ import {
135
143
  optionalServiceSpec,
136
144
  packagedDetachedJobPath,
137
145
  parseDurationMs,
146
+ parseIssueSince,
138
147
  pendingInjectedNotices,
139
148
  prependQueuedItem,
149
+ previewIssueCommentBody,
140
150
  previewLand,
141
151
  readRunLog,
142
152
  readSetupLog,
143
153
  readSkillBody,
144
154
  readTurnLive,
155
+ readTurnLiveParts,
145
156
  readWorktreeFile,
146
157
  readWorktreeFileForUpload,
147
158
  recordScheduleRun,
159
+ registerViewerContextTools,
148
160
  releaseDesktopHost,
149
161
  removeQueuedItem,
150
162
  requireAgent,
@@ -181,11 +193,12 @@ import {
181
193
  verifyOptionalService,
182
194
  visibleAgentBlock,
183
195
  waitForPidExit,
196
+ workspaceTagTargetRef,
184
197
  worktreeAgentBlock,
185
198
  worktreeBoardStatus,
186
199
  worktreeCleanupSettings,
187
200
  writeWorktreeFile
188
- } from "./chunk-DK4DKQ2H.js";
201
+ } from "./chunk-2KNO2GJQ.js";
189
202
  import {
190
203
  BASE_ALLOWED_TOOLS,
191
204
  BRIGHTSY_MCP_ALLOWED_TOOLS,
@@ -275,7 +288,7 @@ import {
275
288
  whichOnPath,
276
289
  worktreeMcpToolNames,
277
290
  writeInjectedMcpConfig
278
- } from "./chunk-HNNS25SG.js";
291
+ } from "./chunk-OCUIZD4Y.js";
279
292
  import {
280
293
  brightsyMcpServerName,
281
294
  connectBrightsyTeam,
@@ -402,7 +415,7 @@ import {
402
415
  workspaceAgentChatSummaries,
403
416
  worktreeRunPortsPath,
404
417
  wrapReviewSkillMarkdown
405
- } from "./chunk-HQXBBUQX.js";
418
+ } from "./chunk-DQZPF6WL.js";
406
419
  import {
407
420
  STDERR_EVENT_MAX_CHARS,
408
421
  STDOUT_EVENT_MAX_CHARS,
@@ -444,7 +457,7 @@ import {
444
457
  visibleToolRowDetail,
445
458
  withEventParentId,
446
459
  withEventsParentId
447
- } from "./chunk-Y7JPL4WA.js";
460
+ } from "./chunk-BDAUPNHL.js";
448
461
  import "./chunk-7H655NEW.js";
449
462
  import {
450
463
  ABLETIME_OAUTH_CALLBACK_PATH,
@@ -468,7 +481,7 @@ import {
468
481
  parseAbleTimeOAuthCallbackUrl,
469
482
  resolveAbleTimeOAuthHost,
470
483
  startAbleTimeOAuth
471
- } from "./chunk-QINJKNLR.js";
484
+ } from "./chunk-XTPQEZZA.js";
472
485
  import {
473
486
  ableTimeDependencyTarget,
474
487
  commentAbleTimeTask,
@@ -493,8 +506,8 @@ import {
493
506
  toAbleTimeIssueInfo,
494
507
  updateAbleTimeTask,
495
508
  verifyAbleTimeConnection
496
- } from "./chunk-2J34G3WJ.js";
497
- import "./chunk-NQ7JMVSU.js";
509
+ } from "./chunk-DGPROCE5.js";
510
+ import "./chunk-U7HAVAGJ.js";
498
511
  import {
499
512
  ABLETIME_MCP_PATH,
500
513
  ABLETIME_MCP_PM_PATH,
@@ -515,7 +528,7 @@ import {
515
528
  normalizeAbleTimeCredential,
516
529
  normalizeAbleTimeHost,
517
530
  rewriteAbleTimeError
518
- } from "./chunk-PZURG3XN.js";
531
+ } from "./chunk-VA55JG2I.js";
519
532
  import {
520
533
  LEGACY_PLAN_FILE_REL,
521
534
  PLAN_FILE_NAME,
@@ -564,6 +577,7 @@ import {
564
577
  isGlobalThread,
565
578
  isImageFilePath,
566
579
  isOrchestratorThread,
580
+ isRemovedWorkspace,
567
581
  isSlackCoordinatorThread,
568
582
  listGlobalThreads,
569
583
  listWorkspaces,
@@ -574,23 +588,21 @@ import {
574
588
  primaryCheckoutFromLinkedWorktree,
575
589
  remoteCoordinatorSourceRef,
576
590
  removeWorkspace,
591
+ sameWorkspacePath,
577
592
  slackCoordinatorSourceRef,
578
593
  stageAbsolutePathsAsAttachments,
579
594
  stageBuffersAsAttachments,
580
595
  syncWorkspacesFromThreads,
581
596
  takenTeamSlugsForOrchestration
582
- } from "./chunk-SKSOCIJT.js";
597
+ } from "./chunk-WY2IAB7J.js";
583
598
  import {
584
- FAMOUS_SOCCER_TEAMS,
585
599
  GITHUB_PR_BODY_MAX_CHARS,
586
600
  GITHUB_PR_BODY_SAFE_CHARS,
587
601
  PR_DETAILS_JSON_FIELDS,
588
602
  addPrStackLayer,
589
- allocateTeamName,
590
603
  allocateTeamSlug,
591
604
  appendIndexedGitConfig,
592
605
  applyGithubGitAuthEnv,
593
- branchDisplayLabel,
594
606
  canonicalizeRepoPath,
595
607
  checkoutPrStackLayer,
596
608
  clampGithubPrBody,
@@ -633,19 +645,16 @@ import {
633
645
  isGhPrBodyTooLongError,
634
646
  isGhRateLimitError,
635
647
  isInPrStack,
636
- isPlaceholderBranch,
637
648
  isPrNotMergeableError,
638
649
  isSideboardScratchPath,
639
650
  listBranches,
640
651
  listPrs,
641
652
  listWorktrees,
642
- lookupSoccerTeam,
643
653
  markPrReady,
644
654
  mergeAgentGitAuthEnv,
645
655
  mergePr,
646
656
  mergePrStack,
647
657
  nonInteractiveGitProcessEnv,
648
- normalizeWorktreePath,
649
658
  originFetchBranch,
650
659
  originGhRepoEnv,
651
660
  parseGhStackViewJson,
@@ -672,32 +681,40 @@ import {
672
681
  submitPrStack,
673
682
  suggestSlug,
674
683
  switchWorktreeBranch,
675
- threadDisplayLabel,
676
- ticketSlugForBranch,
677
- warmGithubAgentAuth,
678
- worktreeDisplayLabel,
679
- worktreeDisplayLabelForGroup,
680
- worktreeNameFromPath,
681
- worktreeSlugForTicket
682
- } from "./chunk-IPPY3FG5.js";
684
+ warmGithubAgentAuth
685
+ } from "./chunk-K5XSTHHP.js";
683
686
  import {
687
+ FAMOUS_SOCCER_TEAMS,
688
+ allocateTeamName,
684
689
  appendMessage,
690
+ branchDisplayLabel,
685
691
  createEmptyThread,
686
692
  deleteThreadRecord,
687
693
  findThreadByRef,
688
694
  invalidateThreadListCache,
689
695
  invalidateThreadRecord,
696
+ isPlaceholderBranch,
690
697
  isSelfWrittenRecord,
691
698
  isThreadRecordFile,
692
699
  listThreads,
700
+ lookupSoccerTeam,
693
701
  normalizeThread,
702
+ normalizeWorkspaceTags,
703
+ normalizeWorktreePath,
694
704
  readThread,
695
705
  resolveThreadEffort,
696
706
  setStatus,
707
+ threadDisplayLabel,
708
+ ticketSlugForBranch,
697
709
  updateThread,
698
710
  withThreadLock,
711
+ workspaceTagsFromGroup,
712
+ worktreeDisplayLabel,
713
+ worktreeDisplayLabelForGroup,
714
+ worktreeNameFromPath,
715
+ worktreeSlugForTicket,
699
716
  writeThread
700
- } from "./chunk-FWNL62VA.js";
717
+ } from "./chunk-N5UGX3JL.js";
701
718
  import "./chunk-2P4XKWV2.js";
702
719
  import {
703
720
  BRIGHTSY_OAUTH_CLIENT_ID,
@@ -1189,18 +1206,18 @@ function lastVisibleMessage(messages) {
1189
1206
  const m = messages[i];
1190
1207
  if (!m) continue;
1191
1208
  if (m.role === "agent") {
1192
- const text6 = (lastAssistantMessageText(m.parts) || m.text || "").trim();
1193
- if (text6 && !isInternalAgentStatusText(text6)) return m;
1209
+ const text5 = (lastAssistantMessageText(m.parts) || m.text || "").trim();
1210
+ if (text5 && !isInternalAgentStatusText(text5)) return m;
1194
1211
  continue;
1195
1212
  }
1196
1213
  if (m.role === "user" && m.text?.trim()) return m;
1197
1214
  }
1198
1215
  return void 0;
1199
1216
  }
1200
- function agentStub(source, text6, usage) {
1217
+ function agentStub(source, text5, usage) {
1201
1218
  return {
1202
1219
  role: "agent",
1203
- text: text6,
1220
+ text: text5,
1204
1221
  ts: source.ts,
1205
1222
  ...usage ? { usage } : {}
1206
1223
  };
@@ -1231,9 +1248,9 @@ async function getGitHubStatus() {
1231
1248
  };
1232
1249
  }
1233
1250
  const status = await run("gh", ["auth", "status"], { reject: false });
1234
- const text6 = `${status.stdout}
1251
+ const text5 = `${status.stdout}
1235
1252
  ${status.stderr}`;
1236
- const loginMatch = text6.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text6.match(/Logged in to ([^\s]+) as (\S+)/i);
1253
+ const loginMatch = text5.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text5.match(/Logged in to ([^\s]+) as (\S+)/i);
1237
1254
  if (loginMatch) {
1238
1255
  return {
1239
1256
  connected: true,
@@ -1253,76 +1270,6 @@ async function refreshGitHubAuth() {
1253
1270
  return getGitHubStatus();
1254
1271
  }
1255
1272
 
1256
- // src/integrations/issue-since.ts
1257
- var ISSUE_SINCE_ERROR = "updatedSince must be an ISO datetime, YYYY-MM-DD, or relative time (yesterday, 2d, 3 hours ago)";
1258
- var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
1259
- var DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
1260
- var RELATIVE_RE = /^(\d+)\s*(hours?|hrs?|h|days?|d|weeks?|wks?|w)(?:\s+ago)?$/i;
1261
- function startOfLocalDay(date) {
1262
- return new Date(date.getFullYear(), date.getMonth(), date.getDate());
1263
- }
1264
- function addLocalDays(date, days) {
1265
- return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
1266
- }
1267
- function parseIssueSince(input, now = /* @__PURE__ */ new Date()) {
1268
- const raw = input.trim();
1269
- if (!raw) throw new Error(ISSUE_SINCE_ERROR);
1270
- const dateOnly = raw.match(DATE_ONLY_RE);
1271
- if (dateOnly) {
1272
- const year = Number(dateOnly[1]);
1273
- const month = Number(dateOnly[2]);
1274
- const day = Number(dateOnly[3]);
1275
- const local = new Date(year, month - 1, day);
1276
- if (local.getFullYear() !== year || local.getMonth() !== month - 1 || local.getDate() !== day) {
1277
- throw new Error(ISSUE_SINCE_ERROR);
1278
- }
1279
- return startOfLocalDay(local).toISOString();
1280
- }
1281
- if (DATE_TIME_RE.test(raw)) {
1282
- const ms = Date.parse(raw);
1283
- if (!Number.isFinite(ms)) throw new Error(ISSUE_SINCE_ERROR);
1284
- return new Date(ms).toISOString();
1285
- }
1286
- const lower = raw.toLowerCase();
1287
- if (lower === "today") return startOfLocalDay(now).toISOString();
1288
- if (lower === "yesterday") return addLocalDays(startOfLocalDay(now), -1).toISOString();
1289
- if (lower === "last week" || lower === "lastweek") {
1290
- return addLocalDays(startOfLocalDay(now), -7).toISOString();
1291
- }
1292
- const relative = lower.match(RELATIVE_RE);
1293
- if (relative) {
1294
- const n = Number(relative[1]);
1295
- if (!Number.isInteger(n) || n < 1) throw new Error(ISSUE_SINCE_ERROR);
1296
- const unit = relative[2];
1297
- if (unit.startsWith("h")) return new Date(now.getTime() - n * 36e5).toISOString();
1298
- if (unit.startsWith("w")) return addLocalDays(startOfLocalDay(now), -7 * n).toISOString();
1299
- return addLocalDays(startOfLocalDay(now), -n).toISOString();
1300
- }
1301
- throw new Error(ISSUE_SINCE_ERROR);
1302
- }
1303
- function formatGitHubSearchUpdatedSince(iso) {
1304
- return iso.replace(/\.\d{3}Z$/, "Z");
1305
- }
1306
- function issueMatchesUpdatedSince(issue, sinceIso) {
1307
- const ts = issue.updatedAt || issue.createdAt;
1308
- if (!ts) return false;
1309
- const value = Date.parse(ts);
1310
- const since = Date.parse(sinceIso);
1311
- return Number.isFinite(value) && Number.isFinite(since) && value >= since;
1312
- }
1313
- var ISSUE_COMMENT_PREVIEW_CHARS = 240;
1314
- function previewIssueCommentBody(body) {
1315
- const flat = body.replace(/\s+/g, " ").trim();
1316
- if (flat.length <= ISSUE_COMMENT_PREVIEW_CHARS) return flat;
1317
- return `${flat.slice(0, ISSUE_COMMENT_PREVIEW_CHARS - 1).trimEnd()}\u2026`;
1318
- }
1319
- function issueActivityKind(issue, sinceIso) {
1320
- const created = issue.createdAt ? Date.parse(issue.createdAt) : Number.NaN;
1321
- const since = Date.parse(sinceIso);
1322
- if (Number.isFinite(created) && Number.isFinite(since) && created >= since) return "created";
1323
- return "updated";
1324
- }
1325
-
1326
1273
  // src/integrations/linear-oauth.ts
1327
1274
  import { createHash, randomBytes } from "crypto";
1328
1275
  import { createServer } from "http";
@@ -1737,6 +1684,7 @@ var LIST_ISSUE_FIELDS = `
1737
1684
  team { id key }
1738
1685
  labels(first: 10) { nodes { name } }
1739
1686
  cycle { id name number startsAt endsAt completedAt }
1687
+ project { id name }
1740
1688
  `;
1741
1689
  var ISSUE_REF_FIELDS = `
1742
1690
  id
@@ -1913,9 +1861,10 @@ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
1913
1861
  }
1914
1862
  `;
1915
1863
  var PROJECTS_QUERY = `
1916
- query SideboardProjects($first: Int!, $filter: ProjectFilter) {
1917
- projects(first: $first, filter: $filter) {
1864
+ query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
1865
+ projects(first: $first, filter: $filter, after: $after) {
1918
1866
  nodes { id name slugId }
1867
+ pageInfo { hasNextPage endCursor }
1919
1868
  }
1920
1869
  }
1921
1870
  `;
@@ -2137,6 +2086,7 @@ function toIssueInfo(issue) {
2137
2086
  assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
2138
2087
  cycle: issue.cycle ?? null,
2139
2088
  teamKey: issue.team?.key || void 0,
2089
+ ...issue.project ? { projects: [issue.project] } : {},
2140
2090
  ...issue.createdAt ? { createdAt: issue.createdAt } : {},
2141
2091
  ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
2142
2092
  };
@@ -2302,30 +2252,35 @@ async function listLinearLabels(opts) {
2302
2252
  }
2303
2253
  return listLinearWorkspaceLabels(opts);
2304
2254
  }
2305
- async function listLinearProjects(filter, opts) {
2306
- const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
2307
- return (json.projects?.nodes ?? []).flatMap((node) => {
2308
- const id = String(node.id ?? "").trim();
2309
- if (!id) return [];
2310
- return [
2311
- {
2312
- id,
2313
- name: String(node.name ?? ""),
2314
- ...node.slugId ? { slugId: String(node.slugId) } : {}
2315
- }
2316
- ];
2317
- });
2255
+ async function queryLinearProjects(filter, opts) {
2256
+ const out = [];
2257
+ let after;
2258
+ for (let page = 0; page < 4; page++) {
2259
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
2260
+ for (const node of json.projects?.nodes ?? []) {
2261
+ const id = String(node.id ?? "").trim();
2262
+ if (!id) continue;
2263
+ out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
2264
+ }
2265
+ const pageInfo = json.projects?.pageInfo;
2266
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
2267
+ after = pageInfo.endCursor;
2268
+ }
2269
+ return out;
2270
+ }
2271
+ function listLinearProjects(opts) {
2272
+ return queryLinearProjects(void 0, opts);
2318
2273
  }
2319
2274
  async function resolveLinearProjectId(project, opts) {
2320
2275
  if (isLinearNoneToken(project)) return null;
2321
2276
  const s = (project ?? "").trim();
2322
2277
  const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
2323
- const matched = await listLinearProjects(filter, opts);
2278
+ const matched = await queryLinearProjects(filter, opts);
2324
2279
  try {
2325
2280
  return resolveLinearProject(matched, s);
2326
2281
  } catch {
2327
2282
  if (UUID_RE.test(s)) return s;
2328
- const listed = await listLinearProjects(void 0, opts);
2283
+ const listed = await queryLinearProjects(void 0, opts);
2329
2284
  return resolveLinearProject(listed, s);
2330
2285
  }
2331
2286
  }
@@ -2718,7 +2673,7 @@ function extractGitHubIssueAttachments(issue) {
2718
2673
  const texts = [issue.body ?? "", ...issue.comments.map((comment) => comment.body)];
2719
2674
  return mergeIssueAttachments(
2720
2675
  texts.flatMap(
2721
- (text6) => extractHttpUrls(text6).filter(isGitHubIssueAttachmentUrl).map((url) => ({
2676
+ (text5) => extractHttpUrls(text5).filter(isGitHubIssueAttachmentUrl).map((url) => ({
2722
2677
  id: url,
2723
2678
  name: url.split("/").pop() || "attachment",
2724
2679
  url
@@ -2747,6 +2702,129 @@ async function downloadGitHubIssueAttachment(input, opts) {
2747
2702
  });
2748
2703
  }
2749
2704
 
2705
+ // src/integrations/github-projects.ts
2706
+ function rewriteGitHubProjectReadError(detail) {
2707
+ const text5 = detail.trim() || "gh could not read GitHub Projects";
2708
+ if (/read:project|project scope|missing required scopes/i.test(text5)) {
2709
+ return `${text5} \u2014 run \`gh auth refresh -s read:project\` so Sideboard can read GitHub Projects.`;
2710
+ }
2711
+ return text5;
2712
+ }
2713
+ function githubProjectsUnreadable(result) {
2714
+ return /project|scope|unknown json field/i.test(`${result.stderr}
2715
+ ${result.stdout}`);
2716
+ }
2717
+ function githubProjectsOnIssue(raw) {
2718
+ const out = [];
2719
+ const seen = /* @__PURE__ */ new Set();
2720
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
2721
+ if (!Array.isArray(fromItems)) return out;
2722
+ for (const item of fromItems) {
2723
+ if (typeof item === "string") {
2724
+ const name2 = item.trim();
2725
+ const key2 = name2.toLowerCase();
2726
+ if (!name2 || seen.has(key2)) continue;
2727
+ seen.add(key2);
2728
+ out.push({ name: name2 });
2729
+ continue;
2730
+ }
2731
+ if (!item || typeof item !== "object") continue;
2732
+ const rec = item;
2733
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
2734
+ const id = String(nested.id ?? rec.id ?? "").trim();
2735
+ const name = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
2736
+ const label = name || id;
2737
+ const key = label.toLowerCase();
2738
+ if (!label || seen.has(key)) continue;
2739
+ seen.add(key);
2740
+ out.push({ ...id ? { id } : {}, name: label });
2741
+ }
2742
+ return out;
2743
+ }
2744
+ async function readGitHubIssueProjects(number, cwd, repoArgs) {
2745
+ const result = await gh(
2746
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
2747
+ cwd,
2748
+ { reject: false }
2749
+ );
2750
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
2751
+ const detail = (result.stderr || result.stdout).trim();
2752
+ if (detail && githubProjectsUnreadable(result)) {
2753
+ return { projects: [], error: rewriteGitHubProjectReadError(detail) };
2754
+ }
2755
+ return { projects: [] };
2756
+ }
2757
+ try {
2758
+ const parsed = JSON.parse(result.stdout);
2759
+ return { projects: githubProjectsOnIssue(parsed) };
2760
+ } catch {
2761
+ return { projects: [] };
2762
+ }
2763
+ }
2764
+ async function listGitHubIssueProjectNames(number, cwd, repoArgs) {
2765
+ const read = await readGitHubIssueProjects(number, cwd, repoArgs);
2766
+ return read.projects.map((project) => project.name).filter(Boolean);
2767
+ }
2768
+ function parseGitHubProjectList(stdout) {
2769
+ let parsed;
2770
+ try {
2771
+ parsed = JSON.parse(stdout);
2772
+ } catch {
2773
+ return [];
2774
+ }
2775
+ const rows = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.projects) ? parsed.projects ?? [] : [];
2776
+ const out = [];
2777
+ for (const row2 of rows) {
2778
+ if (!row2 || typeof row2 !== "object") continue;
2779
+ const rec = row2;
2780
+ const title = String(rec.title ?? rec.name ?? "").trim();
2781
+ if (!title) continue;
2782
+ const ownerRec = rec.owner && typeof rec.owner === "object" ? rec.owner : null;
2783
+ const owner = ownerRec?.login?.trim() || (typeof rec.owner === "string" ? rec.owner.trim() : "");
2784
+ const number = Number(rec.number);
2785
+ out.push({
2786
+ ...typeof rec.id === "string" && rec.id.trim() ? { id: rec.id.trim() } : {},
2787
+ ...Number.isFinite(number) ? { number } : {},
2788
+ title,
2789
+ ...typeof rec.url === "string" && rec.url.trim() ? { url: rec.url.trim() } : {},
2790
+ ...typeof rec.closed === "boolean" ? { closed: rec.closed } : {},
2791
+ ...owner ? { owner } : {}
2792
+ });
2793
+ }
2794
+ return out;
2795
+ }
2796
+ async function listGitHubProjects(opts) {
2797
+ const cwd = await resolveRepoRoot((opts?.repoPath ?? "").trim() || process.cwd());
2798
+ const slug = await resolveGithubRepoSlug(cwd);
2799
+ const owners = /* @__PURE__ */ new Set([""]);
2800
+ const repoOwner = slug?.split("/")[0]?.trim();
2801
+ if (repoOwner) owners.add(repoOwner);
2802
+ const out = [];
2803
+ const seen = /* @__PURE__ */ new Set();
2804
+ let lastError = "";
2805
+ let anyOk = false;
2806
+ for (const owner of owners) {
2807
+ const args = ["project", "list", "--limit", "100", "--format", "json"];
2808
+ if (owner) args.push("--owner", owner);
2809
+ const result = await gh(args, cwd, { reject: false });
2810
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
2811
+ lastError = (result.stderr || result.stdout).trim() || lastError;
2812
+ continue;
2813
+ }
2814
+ anyOk = true;
2815
+ for (const project of parseGitHubProjectList(result.stdout)) {
2816
+ const key = (project.id || `${project.owner ?? ""}:${project.number ?? ""}:${project.title}`).toLowerCase();
2817
+ if (seen.has(key)) continue;
2818
+ seen.add(key);
2819
+ out.push(project);
2820
+ }
2821
+ }
2822
+ if (!anyOk) {
2823
+ throw new Error(rewriteGitHubProjectReadError(lastError || "gh project list failed"));
2824
+ }
2825
+ return out;
2826
+ }
2827
+
2750
2828
  // src/integrations/github-issues.ts
2751
2829
  function requireGhOk(result, label) {
2752
2830
  if (result.exitCode !== 0) {
@@ -2830,7 +2908,8 @@ function toGitHubIssueInfo(issue) {
2830
2908
  labels: issue.labels,
2831
2909
  provider: "github",
2832
2910
  assignee: issue.assignees[0],
2833
- assignees: issue.assignees.length ? issue.assignees : void 0
2911
+ assignees: issue.assignees.length ? issue.assignees : void 0,
2912
+ ...issue.projects?.length ? { projects: issue.projects } : {}
2834
2913
  };
2835
2914
  }
2836
2915
  function issueNumberFromApiUrl(url) {
@@ -2925,7 +3004,11 @@ async function getGitHubIssue(id, opts) {
2925
3004
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2926
3005
  throw new Error(`GitHub issue not found: #${number}`);
2927
3006
  }
2928
- return toGitHubIssue(parsed);
3007
+ const issue = toGitHubIssue(parsed);
3008
+ const projects = await readGitHubIssueProjects(number, cwd, repoArgs);
3009
+ issue.projects = projects.projects;
3010
+ if (projects.error) issue.projectsError = projects.error;
3011
+ return issue;
2929
3012
  }
2930
3013
  async function commentGitHubIssue(input, opts) {
2931
3014
  const number = parseGitHubIssueNumber(input.id);
@@ -2963,46 +3046,6 @@ function githubRelationEditFlag(type, remove = false) {
2963
3046
  function labelsEqual(a, b) {
2964
3047
  return a.trim().toLowerCase() === b.trim().toLowerCase();
2965
3048
  }
2966
- function parseProjectTitles(raw) {
2967
- const out = [];
2968
- const seen = /* @__PURE__ */ new Set();
2969
- const push = (name) => {
2970
- const title = name.trim();
2971
- const key = title.toLowerCase();
2972
- if (!title || seen.has(key)) return;
2973
- seen.add(key);
2974
- out.push(title);
2975
- };
2976
- const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
2977
- if (Array.isArray(fromItems)) {
2978
- for (const item of fromItems) {
2979
- if (typeof item === "string") {
2980
- push(item);
2981
- continue;
2982
- }
2983
- if (!item || typeof item !== "object") continue;
2984
- const rec = item;
2985
- const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
2986
- const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
2987
- if (title) push(title);
2988
- }
2989
- }
2990
- return out;
2991
- }
2992
- async function listGitHubIssueProjects(number, cwd, repoArgs) {
2993
- const result = await gh(
2994
- ["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
2995
- cwd,
2996
- { reject: false }
2997
- );
2998
- if (result.exitCode !== 0 || !result.stdout.trim()) return [];
2999
- try {
3000
- const parsed = JSON.parse(result.stdout);
3001
- return parseProjectTitles(parsed);
3002
- } catch {
3003
- return [];
3004
- }
3005
- }
3006
3049
  async function updateGitHubIssue(input, opts) {
3007
3050
  const number = parseGitHubIssueNumber(input.id);
3008
3051
  const title = input.title?.trim();
@@ -3042,7 +3085,7 @@ async function updateGitHubIssue(input, opts) {
3042
3085
  if (remove.length) args.push("--remove-label", remove.join(","));
3043
3086
  }
3044
3087
  if (input.project !== void 0) {
3045
- const current = await listGitHubIssueProjects(number, cwd, repoArgs);
3088
+ const current = await listGitHubIssueProjectNames(number, cwd, repoArgs);
3046
3089
  if (isGitHubNoneToken(input.project)) {
3047
3090
  for (const name of current) args.push("--remove-project", name);
3048
3091
  } else {
@@ -3126,32 +3169,43 @@ function issueMatchesAssignee(issue, assignee, viewerName = "") {
3126
3169
  async function listGitHubIssues(repoPath, opts) {
3127
3170
  const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
3128
3171
  const slug = await resolveGithubRepoSlug(repoPath);
3129
- const args = [
3130
- "issue",
3131
- "list",
3132
- "--json",
3133
- "number,title,url,labels,assignees,createdAt,updatedAt",
3134
- "--limit",
3135
- String(limit),
3136
- "--state",
3137
- "open"
3138
- ];
3139
- const query = opts?.query?.trim() ?? "";
3140
- const assignee = opts?.assignee?.trim() ?? "";
3141
- const key = assignee.toLowerCase();
3142
- const searchParts = [];
3143
- if (query) searchParts.push(query);
3144
- if (opts?.updatedSince) {
3145
- searchParts.push(`updated:>=${formatGitHubSearchUpdatedSince(opts.updatedSince)}`);
3146
- }
3147
- if (key === "unassigned" || key === "none" || key === "null") {
3148
- searchParts.push("no:assignee");
3149
- } else if (key && key !== "all" && key !== "*") {
3150
- args.push("--assignee", key === "me" || key === "@me" ? "@me" : assignee);
3172
+ const plainFields = "number,title,url,labels,assignees,createdAt,updatedAt";
3173
+ const buildArgs = (jsonFields) => {
3174
+ const args = [
3175
+ "issue",
3176
+ "list",
3177
+ "--json",
3178
+ jsonFields,
3179
+ "--limit",
3180
+ String(limit),
3181
+ "--state",
3182
+ "open"
3183
+ ];
3184
+ const query = opts?.query?.trim() ?? "";
3185
+ const assignee = opts?.assignee?.trim() ?? "";
3186
+ const key = assignee.toLowerCase();
3187
+ const searchParts = [];
3188
+ if (query) searchParts.push(query);
3189
+ if (opts?.updatedSince) {
3190
+ searchParts.push(`updated:>=${formatGitHubSearchUpdatedSince(opts.updatedSince)}`);
3191
+ }
3192
+ if (key === "unassigned" || key === "none" || key === "null") {
3193
+ searchParts.push("no:assignee");
3194
+ } else if (key && key !== "all" && key !== "*") {
3195
+ args.push("--assignee", key === "me" || key === "@me" ? "@me" : assignee);
3196
+ }
3197
+ if (searchParts.length) args.push("--search", searchParts.join(" "));
3198
+ if (slug) args.push("--repo", slug);
3199
+ return args;
3200
+ };
3201
+ let { stdout, exitCode } = await gh(
3202
+ buildArgs(`${plainFields},projectItems`),
3203
+ repoPath,
3204
+ { reject: false }
3205
+ );
3206
+ if (exitCode !== 0) {
3207
+ ({ stdout, exitCode } = await gh(buildArgs(plainFields), repoPath, { reject: false }));
3151
3208
  }
3152
- if (searchParts.length) args.push("--search", searchParts.join(" "));
3153
- if (slug) args.push("--repo", slug);
3154
- const { stdout, exitCode } = await gh(args, repoPath, { reject: false });
3155
3209
  if (exitCode !== 0 || !stdout.trim()) return [];
3156
3210
  let parsed;
3157
3211
  try {
@@ -3170,6 +3224,9 @@ async function listGitHubIssues(repoPath, opts) {
3170
3224
  const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
3171
3225
  const createdAt = item.createdAt?.trim() || void 0;
3172
3226
  const updatedAt = item.updatedAt?.trim() || void 0;
3227
+ const record = raw;
3228
+ const projects = githubProjectsOnIssue(record);
3229
+ const readProjects = "projectItems" in record || "projects" in record || "projectItemsV2" in record;
3173
3230
  return {
3174
3231
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
3175
3232
  identifier,
@@ -3179,6 +3236,7 @@ async function listGitHubIssues(repoPath, opts) {
3179
3236
  provider: "github",
3180
3237
  assignee: assignees[0],
3181
3238
  assignees,
3239
+ ...readProjects ? { projects } : {},
3182
3240
  ...createdAt ? { createdAt } : {},
3183
3241
  ...updatedAt ? { updatedAt } : {}
3184
3242
  };
@@ -3411,11 +3469,11 @@ function fenceLanguage(path2, language) {
3411
3469
  }
3412
3470
  function buildCodeRefAttachment(input) {
3413
3471
  const path2 = input.path.trim();
3414
- const text6 = input.text.replace(/\n$/, "");
3472
+ const text5 = input.text.replace(/\n$/, "");
3415
3473
  if (!path2) {
3416
3474
  throw new Error("code reference requires a file path");
3417
3475
  }
3418
- if (!text6.trim()) {
3476
+ if (!text5.trim()) {
3419
3477
  throw new Error("code reference requires selected text");
3420
3478
  }
3421
3479
  if (input.startLine < 1 || input.endLine < 1 || input.endLine < input.startLine) {
@@ -3428,7 +3486,7 @@ function buildCodeRefAttachment(input) {
3428
3486
  `Referenced code from \`${path2}\` (${range}).`,
3429
3487
  "",
3430
3488
  fence,
3431
- text6,
3489
+ text5,
3432
3490
  "```"
3433
3491
  ].join("\n");
3434
3492
  return {
@@ -3482,16 +3540,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
3482
3540
  var PASTE_ATTACH_MIN_LINES = 15;
3483
3541
  var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
3484
3542
  var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
3485
- function pastedTextStats(text6) {
3486
- const chars = text6.length;
3543
+ function pastedTextStats(text5) {
3544
+ const chars = text5.length;
3487
3545
  if (chars === 0) return { chars: 0, lines: 0 };
3488
- const lines = text6.split(/\r\n|\r|\n/).length;
3546
+ const lines = text5.split(/\r\n|\r|\n/).length;
3489
3547
  return { chars, lines };
3490
3548
  }
3491
- function shouldAttachPastedText(text6) {
3492
- const trimmed = text6.trim();
3549
+ function shouldAttachPastedText(text5) {
3550
+ const trimmed = text5.trim();
3493
3551
  if (!trimmed) return false;
3494
- const { chars, lines } = pastedTextStats(text6);
3552
+ const { chars, lines } = pastedTextStats(text5);
3495
3553
  return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
3496
3554
  }
3497
3555
  function nextPastedTextName(existing) {
@@ -3502,13 +3560,13 @@ function nextPastedTextName(existing) {
3502
3560
  }
3503
3561
  return `Pasted text #${max + 1}.txt`;
3504
3562
  }
3505
- function buildPastedTextAttachment(text6, opts) {
3563
+ function buildPastedTextAttachment(text5, opts) {
3506
3564
  return {
3507
3565
  id: opts?.id ?? randomUUID2(),
3508
3566
  name: opts?.name ?? "Pasted text #1.txt",
3509
3567
  kind: "file",
3510
3568
  path: opts?.path,
3511
- content: text6
3569
+ content: text5
3512
3570
  };
3513
3571
  }
3514
3572
 
@@ -3528,8 +3586,8 @@ function parseChatLink(href) {
3528
3586
  const match = CHAT_LINK_RE.exec(href.trim());
3529
3587
  return match?.[1] ?? null;
3530
3588
  }
3531
- function linkifyChatUrls(text6) {
3532
- return text6.replace(BARE_CHAT_LINK_RE, (url) => {
3589
+ function linkifyChatUrls(text5) {
3590
+ return text5.replace(BARE_CHAT_LINK_RE, (url) => {
3533
3591
  const id = parseChatLink(url);
3534
3592
  if (!id) return url;
3535
3593
  const label = id.length > 8 ? id.slice(0, 8) : id;
@@ -3552,8 +3610,8 @@ function outboundReplyFromTurn(result, copy) {
3552
3610
  case "working":
3553
3611
  return copy.failed(`turn still ${result.taskState}`);
3554
3612
  default: {
3555
- const text6 = result.text.trim();
3556
- return text6 || copy.completedEmpty(result.status);
3613
+ const text5 = result.text.trim();
3614
+ return text5 || copy.completedEmpty(result.status);
3557
3615
  }
3558
3616
  }
3559
3617
  }
@@ -3596,7 +3654,7 @@ function createClippedMcpServer() {
3596
3654
  }
3597
3655
 
3598
3656
  // src/mcp/server.ts
3599
- import { z as z8 } from "zod";
3657
+ import { z as z6 } from "zod";
3600
3658
  import { randomUUID as randomUUID5 } from "crypto";
3601
3659
  import { basename as basename2 } from "path";
3602
3660
 
@@ -3622,13 +3680,164 @@ function resolveRunScriptThreadRef(ref) {
3622
3680
  return thread.id;
3623
3681
  }
3624
3682
 
3683
+ // src/mcp/create-thread-agent.ts
3684
+ function resolveOrchCreateThreadOptions(input) {
3685
+ const { resolveNewThreadOptions: resolveNewThreadOptions2 } = input;
3686
+ const account = resolveNewThreadOptions2({});
3687
+ const requested = input.requestedAgent;
3688
+ const parent = input.parentAgent ?? void 0;
3689
+ const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
3690
+ let agent = account.agent;
3691
+ let ignoredAgent;
3692
+ let honorRequestedModel = false;
3693
+ if (requested) {
3694
+ const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
3695
+ const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
3696
+ if (parentEcho || cursorAutofill) {
3697
+ ignoredAgent = requested;
3698
+ } else {
3699
+ agent = requested;
3700
+ honorRequestedModel = input.requestedModel !== void 0;
3701
+ }
3702
+ }
3703
+ let coercedFrom;
3704
+ if (agent === "codex" && parent === "codex" && cursorReady) {
3705
+ coercedFrom = requested ?? "codex";
3706
+ agent = "cursor";
3707
+ }
3708
+ const opts = resolveNewThreadOptions2({
3709
+ agent,
3710
+ model: honorRequestedModel ? input.requestedModel : void 0
3711
+ });
3712
+ return {
3713
+ ...opts,
3714
+ ...ignoredAgent ? { ignoredAgent } : {},
3715
+ ...coercedFrom ? { coercedFrom } : {}
3716
+ };
3717
+ }
3718
+ function orchCreateThreadAgentNote(input) {
3719
+ if (input.coercedFrom) {
3720
+ return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
3721
+ }
3722
+ if (input.ignoredAgent) {
3723
+ return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
3724
+ }
3725
+ return void 0;
3726
+ }
3727
+
3728
+ // src/mcp/create-orch-child.ts
3729
+ var MAX_ORCH_THREADS = 5;
3730
+ var CREATE_THREAD_TIMEOUT_MS = 9e4;
3731
+ function withTimeout(promise, ms, label) {
3732
+ return new Promise((resolve, reject) => {
3733
+ const timer = setTimeout(() => {
3734
+ reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
3735
+ }, ms);
3736
+ promise.then(
3737
+ (value) => {
3738
+ clearTimeout(timer);
3739
+ resolve(value);
3740
+ },
3741
+ (err) => {
3742
+ clearTimeout(timer);
3743
+ reject(err);
3744
+ }
3745
+ );
3746
+ });
3747
+ }
3748
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
3749
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
3750
+ let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
3751
+ let parent = parentId ? orch.getThread(parentId) : null;
3752
+ let parentCorrectedFrom;
3753
+ if (envParentId) {
3754
+ const envParent = orch.getThread(envParentId);
3755
+ if (envParent) {
3756
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
3757
+ else if (!parentId) parentCorrectedFrom = void 0;
3758
+ parentId = envParentId;
3759
+ parent = envParent;
3760
+ }
3761
+ }
3762
+ if (parentId && !parent) {
3763
+ parentCorrectedFrom = parentId;
3764
+ parentId = "";
3765
+ parent = null;
3766
+ }
3767
+ if (parentId) {
3768
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
3769
+ if (children.length >= MAX_ORCH_THREADS) {
3770
+ return {
3771
+ ok: false,
3772
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
3773
+ };
3774
+ }
3775
+ }
3776
+ const opts = resolveOrchCreateThreadOptions({
3777
+ requestedAgent: args.agent,
3778
+ requestedModel: args.model,
3779
+ parentAgent: parent?.agent,
3780
+ resolveNewThreadOptions: resolveNewThreadOptions2
3781
+ });
3782
+ const agentNote = orchCreateThreadAgentNote(opts);
3783
+ try {
3784
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
3785
+ const thread = await withTimeout(
3786
+ orch.createThread({
3787
+ sourceType: args.sourceType,
3788
+ sourceRef: args.sourceRef,
3789
+ agent: opts.agent,
3790
+ model: opts.model,
3791
+ effort: opts.effort,
3792
+ fast: opts.fast,
3793
+ repoPath: args.repoPath,
3794
+ title: args.title,
3795
+ parentThreadId: parentId || null,
3796
+ cowboy: args.cowboy || void 0,
3797
+ attachments: args.attachments,
3798
+ tags: args.tags === void 0 ? void 0 : normalizeWorkspaceTags(args.tags)
3799
+ }),
3800
+ CREATE_THREAD_TIMEOUT_MS,
3801
+ "create_workspace"
3802
+ );
3803
+ const alreadyStarted = priorIds.has(thread.id);
3804
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
3805
+ return {
3806
+ ok: true,
3807
+ text: JSON.stringify({
3808
+ id: thread.id,
3809
+ title: thread.title,
3810
+ branchName: thread.branchName,
3811
+ worktreePath: thread.worktreePath,
3812
+ agent: thread.agent,
3813
+ model: thread.model,
3814
+ status: thread.status,
3815
+ cowboy: Boolean(thread.cowboy),
3816
+ link: `sideboard://chat/${thread.id}`,
3817
+ parentChatId: thread.parentThreadId,
3818
+ parentThreadId: thread.parentThreadId,
3819
+ tags: thread.tags ?? [],
3820
+ chats: workspaceAgentChatSummaries(thread.worktreePath),
3821
+ ...alreadyStarted ? { alreadyStarted: true } : {},
3822
+ ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
3823
+ ...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
3824
+ ...agentNote ? { note: agentNote } : {},
3825
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
3826
+ })
3827
+ };
3828
+ } catch (err) {
3829
+ const message = err instanceof Error ? err.message : String(err);
3830
+ return { ok: false, text: `create_workspace failed: ${message}` };
3831
+ }
3832
+ }
3833
+
3625
3834
  // src/mcp/thread-visibility.ts
3626
3835
  function lastMessagePreview(messages, max = 160) {
3627
3836
  if (!messages?.length) return null;
3628
3837
  for (let i = messages.length - 1; i >= 0; i--) {
3629
- const text6 = messages[i]?.text?.trim();
3630
- if (!text6 || isInternalAgentStatusText(text6) || isInjectedNoticeText(text6)) continue;
3631
- const flat = text6.replace(/\s+/g, " ");
3838
+ const text5 = messages[i]?.text?.trim();
3839
+ if (!text5 || isInternalAgentStatusText(text5) || isInjectedNoticeText(text5)) continue;
3840
+ const flat = text5.replace(/\s+/g, " ");
3632
3841
  return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
3633
3842
  }
3634
3843
  return null;
@@ -3645,79 +3854,10 @@ function childThreadRefs(parentId, threads) {
3645
3854
  }
3646
3855
 
3647
3856
  // src/mcp/abletime-tools.ts
3648
- import { z as z3 } from "zod";
3649
-
3650
- // src/mcp/issue-list.ts
3651
- import { z } from "zod";
3652
- var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
3653
- var MCP_ISSUE_LIST_MAX_LIMIT = 250;
3654
- function clampMcpIssueLimit(limit) {
3655
- if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
3656
- return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
3657
- }
3658
- function applyIssueListWindow(items, limit) {
3659
- const truncated = items.length > limit;
3660
- return {
3661
- items: truncated ? items.slice(0, limit) : items,
3662
- truncated
3663
- };
3664
- }
3665
- var MCP_ISSUE_UPDATED_SINCE_DESCRIPTION = "ISO datetime, YYYY-MM-DD, or relative (yesterday, 2d, 3 hours ago). Tickets updated since then plus new comments. Use for \u201Cany updates since yesterday?\u201D";
3666
- var mcpIssueUpdatedSinceSchema = z.string().min(1).optional().describe(MCP_ISSUE_UPDATED_SINCE_DESCRIPTION);
3667
- function compactIssueRow(issue, opts) {
3668
- return {
3669
- identifier: issue.identifier,
3670
- title: issue.title,
3671
- ...issue.assignee ? { assignee: issue.assignee } : {},
3672
- ...issue.labels.length > 0 ? { labels: issue.labels } : {},
3673
- ...issue.teamKey ? { team: issue.teamKey } : {},
3674
- ...issue.cycle?.name ? { cycle: issue.cycle.name } : {},
3675
- ...issue.createdAt ? { createdAt: issue.createdAt } : {},
3676
- ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {},
3677
- ...opts?.since ? { kind: issueActivityKind(issue, opts.since) } : {}
3678
- };
3679
- }
3680
- function formatMcpIssueList(input) {
3681
- const truncated = input.truncated || Boolean(input.commentsTruncated);
3682
- const comments = input.comments ?? [];
3683
- return {
3684
- ...input.source ? { source: input.source } : {},
3685
- ...input.viewer ? { viewer: input.viewer } : {},
3686
- limit: input.limit,
3687
- returned: input.issues.length,
3688
- truncated,
3689
- ...input.since ? { since: input.since } : {},
3690
- ...truncated ? {
3691
- hint: input.since ? "More issues or comments match. Pass a higher limit (max 250) or a tighter query." : "More issues match. Pass a higher limit (max 250) or a tighter query."
3692
- } : {},
3693
- issues: input.issues.map((issue) => compactIssueRow(issue, { since: input.since })),
3694
- ...input.since || comments.length > 0 ? { comments } : {}
3695
- };
3696
- }
3697
- function formatListedIssuesForMcp(result, page) {
3698
- const windowed = applyIssueListWindow(result.issues, page);
3699
- const comments = result.comments ?? [];
3700
- const commentWindow = applyIssueListWindow(comments, page);
3701
- return formatMcpIssueList({
3702
- source: result.source,
3703
- viewer: result.viewer?.name || result.viewer?.login,
3704
- limit: page,
3705
- issues: windowed.items,
3706
- truncated: windowed.truncated,
3707
- since: result.since,
3708
- comments: commentWindow.items,
3709
- commentsTruncated: commentWindow.truncated
3710
- });
3711
- }
3712
- function mcpJson(payload, isError = false) {
3713
- return {
3714
- content: [{ type: "text", text: JSON.stringify(payload) }],
3715
- ...isError ? { isError: true } : {}
3716
- };
3717
- }
3857
+ import { z as z2 } from "zod";
3718
3858
 
3719
3859
  // src/mcp/issue-payload.ts
3720
- import { z as z2 } from "zod";
3860
+ import { z } from "zod";
3721
3861
 
3722
3862
  // src/mcp/smart-crush.ts
3723
3863
  var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
@@ -3727,15 +3867,15 @@ var CRUSH_COMMENT_MAX_CHARS = 16e3;
3727
3867
  var CRUSH_BODY_MIN_CHARS = 12e3;
3728
3868
  var CRUSH_BODY_MAX_CHARS = 8e3;
3729
3869
  var NEAR_DUP_JACCARD = 0.92;
3730
- function looksLikeMustKeep(text6) {
3731
- return MUST_KEEP_TEXT_RE.test(text6);
3870
+ function looksLikeMustKeep(text5) {
3871
+ return MUST_KEEP_TEXT_RE.test(text5);
3732
3872
  }
3733
- function normalizeText(text6) {
3734
- return text6.replace(/\s+/g, " ").trim().toLowerCase();
3873
+ function normalizeText(text5) {
3874
+ return text5.replace(/\s+/g, " ").trim().toLowerCase();
3735
3875
  }
3736
- function wordSet(text6) {
3876
+ function wordSet(text5) {
3737
3877
  return new Set(
3738
- text6.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
3878
+ text5.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
3739
3879
  );
3740
3880
  }
3741
3881
  function jaccard(a, b) {
@@ -3747,11 +3887,11 @@ function jaccard(a, b) {
3747
3887
  const union = a.size + b.size - inter;
3748
3888
  return union === 0 ? 0 : inter / union;
3749
3889
  }
3750
- function importance(text6) {
3751
- let score = text6.length;
3752
- if (/```/.test(text6)) score += 400;
3753
- if (/https?:\/\//.test(text6)) score += 50;
3754
- if (looksLikeMustKeep(text6)) score += 1e4;
3890
+ function importance(text5) {
3891
+ let score = text5.length;
3892
+ if (/```/.test(text5)) score += 400;
3893
+ if (/https?:\/\//.test(text5)) score += 50;
3894
+ if (looksLikeMustKeep(text5)) score += 1e4;
3755
3895
  return score;
3756
3896
  }
3757
3897
  function meanStd(values) {
@@ -3765,15 +3905,15 @@ function dedupTextItems(items, getText) {
3765
3905
  const keptNorm = [];
3766
3906
  const keptWords = [];
3767
3907
  for (const item of items) {
3768
- const text6 = getText(item);
3769
- const norm = normalizeText(text6);
3908
+ const text5 = getText(item);
3909
+ const norm = normalizeText(text5);
3770
3910
  if (!norm) {
3771
3911
  kept.push(item);
3772
3912
  keptNorm.push(norm);
3773
- keptWords.push(wordSet(text6));
3913
+ keptWords.push(wordSet(text5));
3774
3914
  continue;
3775
3915
  }
3776
- const words = wordSet(text6);
3916
+ const words = wordSet(text5);
3777
3917
  let dupAt = -1;
3778
3918
  for (let i = 0; i < kept.length; i++) {
3779
3919
  if (keptNorm[i] === norm) {
@@ -3791,7 +3931,7 @@ function dedupTextItems(items, getText) {
3791
3931
  keptWords.push(words);
3792
3932
  continue;
3793
3933
  }
3794
- if (text6.length > getText(kept[dupAt]).length) {
3934
+ if (text5.length > getText(kept[dupAt]).length) {
3795
3935
  kept[dupAt] = item;
3796
3936
  keptNorm[dupAt] = norm;
3797
3937
  keptWords[dupAt] = words;
@@ -3832,8 +3972,8 @@ function crushTextItems(items, getText, opts = {}) {
3832
3972
  const outlierFloor = mean + 2 * std;
3833
3973
  const mustKeep = /* @__PURE__ */ new Set();
3834
3974
  unique.forEach((item, i) => {
3835
- const text6 = getText(item);
3836
- if (looksLikeMustKeep(text6) || text6.length > outlierFloor) mustKeep.add(i);
3975
+ const text5 = getText(item);
3976
+ if (looksLikeMustKeep(text5) || text5.length > outlierFloor) mustKeep.add(i);
3837
3977
  });
3838
3978
  const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
3839
3979
  let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
@@ -3875,9 +4015,9 @@ function crushTextItems(items, getText, opts = {}) {
3875
4015
  reason: "budget"
3876
4016
  };
3877
4017
  }
3878
- function splitBlocks(text6) {
3879
- if (/\n\n/.test(text6)) return text6.split(/\n\n+/);
3880
- return text6.split("\n");
4018
+ function splitBlocks(text5) {
4019
+ if (/\n\n/.test(text5)) return text5.split(/\n\n+/);
4020
+ return text5.split("\n");
3881
4021
  }
3882
4022
  function capOversizedBlock(block, maxChars) {
3883
4023
  if (block.length <= maxChars) return block;
@@ -3889,8 +4029,8 @@ function capOversizedBlock(block, maxChars) {
3889
4029
 
3890
4030
  ${block.slice(-tail)}`;
3891
4031
  }
3892
- function crushMarkdown(text6, opts) {
3893
- const original = text6 ?? "";
4032
+ function crushMarkdown(text5, opts) {
4033
+ const original = text5 ?? "";
3894
4034
  const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
3895
4035
  const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
3896
4036
  if (original.length < minChars) {
@@ -3962,7 +4102,7 @@ function crushMarkdown(text6, opts) {
3962
4102
 
3963
4103
  // src/mcp/issue-payload.ts
3964
4104
  var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
3965
- var mcpIssueIncludeSchema = z2.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
4105
+ var mcpIssueIncludeSchema = z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
3966
4106
  "default applies SmartCrusher-style crush (dedup + budget; small unique tickets pass through). full returns the uncompressed vendor payload. Re-call with include=full if crush.truncated."
3967
4107
  );
3968
4108
  var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
@@ -3980,13 +4120,13 @@ function withComments(comments, meta) {
3980
4120
  }
3981
4121
  return { comments: crushed.items, meta };
3982
4122
  }
3983
- function withBody(text6, meta) {
3984
- const crushed = crushMarkdown(text6);
4123
+ function withBody(text5, meta) {
4124
+ const crushed = crushMarkdown(text5);
3985
4125
  if (crushed.truncated) {
3986
4126
  meta.body = { originalChars: crushed.originalChars };
3987
4127
  return { value: crushed.text, meta };
3988
4128
  }
3989
- return { value: text6, meta };
4129
+ return { value: text5, meta };
3990
4130
  }
3991
4131
  function attachCrush(payload, meta) {
3992
4132
  if (!meta.comments && !meta.body) return payload;
@@ -4108,7 +4248,7 @@ function registerAbleTimeTools(server) {
4108
4248
  server.tool(
4109
4249
  "abletime_list_tasks",
4110
4250
  "List open AbleTime tasks (excludes done/archived). Optional projectId filter. Pass updatedSince for tasks updated since then and any comments on that page.",
4111
- { projectId: z3.string().optional(), updatedSince: mcpIssueUpdatedSinceSchema },
4251
+ { projectId: z2.string().optional(), updatedSince: mcpIssueUpdatedSinceSchema },
4112
4252
  async ({ projectId, updatedSince }) => {
4113
4253
  try {
4114
4254
  const tasks = await listAbleTimeTasks({ projectId });
@@ -4121,7 +4261,7 @@ function registerAbleTimeTools(server) {
4121
4261
  server.tool(
4122
4262
  "abletime_search_tasks",
4123
4263
  "Search AbleTime tasks by text, reference, or tag. Pass updatedSince for tasks updated since then and any comments on that page.",
4124
- { query: z3.string(), updatedSince: mcpIssueUpdatedSinceSchema },
4264
+ { query: z2.string(), updatedSince: mcpIssueUpdatedSinceSchema },
4125
4265
  async ({ query, updatedSince }) => {
4126
4266
  try {
4127
4267
  const tasks = await searchAbleTimeTasks(query);
@@ -4133,8 +4273,8 @@ function registerAbleTimeTools(server) {
4133
4273
  );
4134
4274
  server.tool(
4135
4275
  "abletime_get_task",
4136
- "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
4137
- { id: z3.string(), include: mcpIssueIncludeSchema },
4276
+ "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, project, comments, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
4277
+ { id: z2.string(), include: mcpIssueIncludeSchema },
4138
4278
  async ({ id, include }) => {
4139
4279
  try {
4140
4280
  return text(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
@@ -4147,9 +4287,9 @@ function registerAbleTimeTools(server) {
4147
4287
  "abletime_download_attachment",
4148
4288
  "Download an AbleTime task file into `.context/attachments/` using Account AbleTime auth. Pass the attachment id or URL from abletime_get_task. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
4149
4289
  {
4150
- id: z3.string().describe("Attachment id or URL from abletime_get_task."),
4151
- name: z3.string().optional().describe("Optional filename override."),
4152
- repoPath: z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
4290
+ id: z2.string().describe("Attachment id or URL from abletime_get_task."),
4291
+ name: z2.string().optional().describe("Optional filename override."),
4292
+ repoPath: z2.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
4153
4293
  },
4154
4294
  async ({ id, name, repoPath }) => {
4155
4295
  try {
@@ -4162,7 +4302,7 @@ function registerAbleTimeTools(server) {
4162
4302
  server.tool(
4163
4303
  "abletime_comment",
4164
4304
  "Add a markdown comment on an AbleTime task (id or CRM-232). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
4165
- { id: z3.string(), body: z3.string() },
4305
+ { id: z2.string(), body: z2.string() },
4166
4306
  async (args) => {
4167
4307
  try {
4168
4308
  return text(await commentAbleTimeTask(args));
@@ -4171,23 +4311,23 @@ function registerAbleTimeTools(server) {
4171
4311
  }
4172
4312
  }
4173
4313
  );
4174
- const relationInputSchema = z3.object({
4175
- type: z3.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
4176
- issue: z3.string().describe("Related task id or reference (CRM-232).")
4314
+ const relationInputSchema = z2.object({
4315
+ type: z2.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
4316
+ issue: z2.string().describe("Related task id or reference (CRM-232).")
4177
4317
  });
4178
4318
  server.tool(
4179
4319
  "abletime_update_task",
4180
4320
  'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
4181
4321
  {
4182
- id: z3.string(),
4183
- title: z3.string().optional(),
4184
- description: z3.string().optional(),
4185
- state: z3.string().optional(),
4186
- labels: z3.array(z3.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
4187
- project: z3.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
4188
- parent: z3.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
4189
- relations: z3.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
4190
- removeRelations: z3.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
4322
+ id: z2.string(),
4323
+ title: z2.string().optional(),
4324
+ description: z2.string().optional(),
4325
+ state: z2.string().optional(),
4326
+ labels: z2.array(z2.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
4327
+ project: z2.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
4328
+ parent: z2.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
4329
+ relations: z2.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
4330
+ removeRelations: z2.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
4191
4331
  },
4192
4332
  async (args) => {
4193
4333
  try {
@@ -4206,12 +4346,12 @@ function registerAbleTimeTools(server) {
4206
4346
  "abletime_create_task",
4207
4347
  "Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. Pass parent (CRM-232) for a spin-off. New tasks start in todo (or backlog).",
4208
4348
  {
4209
- title: z3.string(),
4210
- description: z3.string().optional(),
4211
- projectId: z3.string().optional(),
4212
- categoryId: z3.string().optional(),
4213
- state: z3.enum(["backlog", "todo"]).optional(),
4214
- parent: z3.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
4349
+ title: z2.string(),
4350
+ description: z2.string().optional(),
4351
+ projectId: z2.string().optional(),
4352
+ categoryId: z2.string().optional(),
4353
+ state: z2.enum(["backlog", "todo"]).optional(),
4354
+ parent: z2.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
4215
4355
  },
4216
4356
  async (args) => {
4217
4357
  try {
@@ -4225,10 +4365,10 @@ function registerAbleTimeTools(server) {
4225
4365
  "abletime_ensure_task",
4226
4366
  "Find or create an AbleTime task for this work, then create_workspace with sourceType=ticket.",
4227
4367
  {
4228
- title: z3.string(),
4229
- description: z3.string().optional(),
4230
- projectId: z3.string().optional(),
4231
- categoryId: z3.string().optional()
4368
+ title: z2.string(),
4369
+ description: z2.string().optional(),
4370
+ projectId: z2.string().optional(),
4371
+ categoryId: z2.string().optional()
4232
4372
  },
4233
4373
  async (args) => {
4234
4374
  try {
@@ -4242,7 +4382,7 @@ function registerAbleTimeTools(server) {
4242
4382
  }
4243
4383
 
4244
4384
  // src/mcp/github-tools.ts
4245
- import { z as z4 } from "zod";
4385
+ import { z as z3 } from "zod";
4246
4386
  function text2(payload, isError = false) {
4247
4387
  return mcpJson(payload, isError);
4248
4388
  }
@@ -4252,15 +4392,15 @@ function fail2(err) {
4252
4392
  true
4253
4393
  );
4254
4394
  }
4255
- var repoPathSchema = z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
4395
+ var repoPathSchema = z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
4256
4396
  function registerGithubIssueTools(server) {
4257
4397
  server.tool(
4258
4398
  "github_search_issues",
4259
4399
  "Search or list open GitHub issues in this repo via Account gh. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all, or a login. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
4260
4400
  {
4261
- query: z4.string().optional().describe("Search text (title, body, identifiers). Omit to list by assignee only."),
4262
- assignee: z4.string().optional().describe("me, unassigned, all, or a GitHub login. Default: all when query is set, otherwise me."),
4263
- limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4401
+ query: z3.string().optional().describe("Search text (title, body, identifiers). Omit to list by assignee only."),
4402
+ assignee: z3.string().optional().describe("me, unassigned, all, or a GitHub login. Default: all when query is set, otherwise me."),
4403
+ limit: z3.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4264
4404
  updatedSince: mcpIssueUpdatedSinceSchema,
4265
4405
  repoPath: repoPathSchema
4266
4406
  },
@@ -4297,10 +4437,22 @@ function registerGithubIssueTools(server) {
4297
4437
  }
4298
4438
  }
4299
4439
  );
4440
+ server.tool(
4441
+ "github_list_projects",
4442
+ "List open GitHub Projects for the signed-in user and this repo owner (title, number, id). Pass a title as project on github_update_issue. If GitHub says the token is missing read:project, run `gh auth refresh -s read:project`.",
4443
+ { repoPath: repoPathSchema },
4444
+ async ({ repoPath }) => {
4445
+ try {
4446
+ return text2({ projects: await listGitHubProjects({ repoPath }) });
4447
+ } catch (err) {
4448
+ return fail2(err);
4449
+ }
4450
+ }
4451
+ );
4300
4452
  server.tool(
4301
4453
  "github_get_issue",
4302
- "Get a GitHub issue (#123 or URL): body, comments, state, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
4303
- { id: z4.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
4454
+ "Get a GitHub issue (#123 or URL): body, comments, state, projects, attachments. Re-fetch to read new comments. projectsError means GitHub refused Projects (usually missing read:project \u2014 run `gh auth refresh -s read:project`). Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
4455
+ { id: z3.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
4304
4456
  async ({ id, include, repoPath }) => {
4305
4457
  try {
4306
4458
  return text2(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
@@ -4313,8 +4465,8 @@ function registerGithubIssueTools(server) {
4313
4465
  "github_download_attachment",
4314
4466
  "Download a GitHub issue file (github.com/user-attachments/\u2026 or githubusercontent.com) into `.context/attachments/` using Account gh. Pass the URL from github_get_issue attachments. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
4315
4467
  {
4316
- url: z4.string().describe("Attachment URL from github_get_issue."),
4317
- name: z4.string().optional().describe("Optional filename override."),
4468
+ url: z3.string().describe("Attachment URL from github_get_issue."),
4469
+ name: z3.string().optional().describe("Optional filename override."),
4318
4470
  repoPath: repoPathSchema
4319
4471
  },
4320
4472
  async ({ url, name, repoPath }) => {
@@ -4328,7 +4480,7 @@ function registerGithubIssueTools(server) {
4328
4480
  server.tool(
4329
4481
  "github_comment",
4330
4482
  "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh. When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
4331
- { id: z4.string(), body: z4.string(), repoPath: repoPathSchema },
4483
+ { id: z3.string(), body: z3.string(), repoPath: repoPathSchema },
4332
4484
  async ({ id, body, repoPath }) => {
4333
4485
  try {
4334
4486
  return text2(await commentGitHubIssue({ id, body }, { repoPath }));
@@ -4337,23 +4489,23 @@ function registerGithubIssueTools(server) {
4337
4489
  }
4338
4490
  }
4339
4491
  );
4340
- const relationInputSchema = z4.object({
4341
- type: z4.string().describe("blocks or blockedBy (this issue's view)."),
4342
- issue: z4.string().describe("Related issue #123 or URL.")
4492
+ const relationInputSchema = z3.object({
4493
+ type: z3.string().describe("blocks or blockedBy (this issue's view)."),
4494
+ issue: z3.string().describe("Related issue #123 or URL.")
4343
4495
  });
4344
4496
  server.tool(
4345
4497
  "github_update_issue",
4346
4498
  'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
4347
4499
  {
4348
- id: z4.string(),
4349
- title: z4.string().optional(),
4350
- body: z4.string().optional(),
4351
- state: z4.string().optional().describe("open or closed"),
4352
- labels: z4.array(z4.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
4353
- project: z4.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
4354
- parent: z4.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
4355
- relations: z4.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
4356
- removeRelations: z4.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
4500
+ id: z3.string(),
4501
+ title: z3.string().optional(),
4502
+ body: z3.string().optional(),
4503
+ state: z3.string().optional().describe("open or closed"),
4504
+ labels: z3.array(z3.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
4505
+ project: z3.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
4506
+ parent: z3.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
4507
+ relations: z3.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
4508
+ removeRelations: z3.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
4357
4509
  repoPath: repoPathSchema
4358
4510
  },
4359
4511
  async (args) => {
@@ -4368,9 +4520,9 @@ function registerGithubIssueTools(server) {
4368
4520
  "github_create_issue",
4369
4521
  "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
4370
4522
  {
4371
- title: z4.string(),
4372
- body: z4.string().optional(),
4373
- parent: z4.string().optional().describe("Parent issue #123 or URL for a spin-off."),
4523
+ title: z3.string(),
4524
+ body: z3.string().optional(),
4525
+ parent: z3.string().optional().describe("Parent issue #123 or URL for a spin-off."),
4374
4526
  repoPath: repoPathSchema
4375
4527
  },
4376
4528
  async (args) => {
@@ -4384,7 +4536,7 @@ function registerGithubIssueTools(server) {
4384
4536
  }
4385
4537
 
4386
4538
  // src/mcp/linear-tools.ts
4387
- import { z as z5 } from "zod";
4539
+ import { z as z4 } from "zod";
4388
4540
  function text3(payload, isError = false) {
4389
4541
  return mcpJson(payload, isError);
4390
4542
  }
@@ -4394,7 +4546,7 @@ function fail3(err) {
4394
4546
  true
4395
4547
  );
4396
4548
  }
4397
- var prioritySchema = z5.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
4549
+ var prioritySchema = z4.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
4398
4550
  function registerLinearTools(server) {
4399
4551
  server.tool(
4400
4552
  "linear_list_teams",
@@ -4408,13 +4560,25 @@ function registerLinearTools(server) {
4408
4560
  }
4409
4561
  }
4410
4562
  );
4563
+ server.tool(
4564
+ "linear_list_projects",
4565
+ "List Linear projects (id, name, slugId). Pass a name or id as project on linear_update_issue. linear_get_issue and search rows also include the project a ticket is on.",
4566
+ {},
4567
+ async () => {
4568
+ try {
4569
+ return text3({ projects: await listLinearProjects() });
4570
+ } catch (err) {
4571
+ return fail3(err);
4572
+ }
4573
+ }
4574
+ );
4411
4575
  server.tool(
4412
4576
  "linear_search_issues",
4413
4577
  "Search or list open Linear issues. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all (default with query), or a user id/name. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
4414
4578
  {
4415
- query: z5.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
4416
- assignee: z5.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
4417
- limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4579
+ query: z4.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
4580
+ assignee: z4.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
4581
+ limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
4418
4582
  updatedSince: mcpIssueUpdatedSinceSchema
4419
4583
  },
4420
4584
  async ({ query, assignee, limit, updatedSince }) => {
@@ -4453,8 +4617,8 @@ function registerLinearTools(server) {
4453
4617
  );
4454
4618
  server.tool(
4455
4619
  "linear_get_issue",
4456
- "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
4457
- { id: z5.string(), include: mcpIssueIncludeSchema },
4620
+ "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, project, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
4621
+ { id: z4.string(), include: mcpIssueIncludeSchema },
4458
4622
  async ({ id, include }) => {
4459
4623
  try {
4460
4624
  return text3(formatLinearIssuePayload(await getLinearIssue(id), include));
@@ -4467,9 +4631,9 @@ function registerLinearTools(server) {
4467
4631
  "linear_download_attachment",
4468
4632
  "Download a Linear file attachment into `.context/attachments/` using Account Linear auth. Pass the attachment uuid or uploads.linear.app URL from linear_get_issue. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
4469
4633
  {
4470
- id: z5.string().describe("Attachment uuid or upload URL from linear_get_issue."),
4471
- name: z5.string().optional().describe("Optional filename override."),
4472
- repoPath: z5.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
4634
+ id: z4.string().describe("Attachment uuid or upload URL from linear_get_issue."),
4635
+ name: z4.string().optional().describe("Optional filename override."),
4636
+ repoPath: z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
4473
4637
  },
4474
4638
  async ({ id, name, repoPath }) => {
4475
4639
  try {
@@ -4483,13 +4647,13 @@ function registerLinearTools(server) {
4483
4647
  "linear_create_issue",
4484
4648
  'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id. Pass parent (ENG-123 or uuid) to nest a spin-off under the current ticket.',
4485
4649
  {
4486
- team: z5.string(),
4487
- title: z5.string(),
4488
- description: z5.string().optional(),
4489
- state: z5.string().optional(),
4490
- assignee: z5.string().nullable().optional(),
4650
+ team: z4.string(),
4651
+ title: z4.string(),
4652
+ description: z4.string().optional(),
4653
+ state: z4.string().optional(),
4654
+ assignee: z4.string().nullable().optional(),
4491
4655
  priority: prioritySchema,
4492
- parent: z5.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
4656
+ parent: z4.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
4493
4657
  },
4494
4658
  async (args) => {
4495
4659
  try {
@@ -4499,26 +4663,26 @@ function registerLinearTools(server) {
4499
4663
  }
4500
4664
  }
4501
4665
  );
4502
- const relationInputSchema = z5.object({
4503
- type: z5.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
4504
- issue: z5.string().describe("Related issue uuid or identifier (ENG-123).")
4666
+ const relationInputSchema = z4.object({
4667
+ type: z4.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
4668
+ issue: z4.string().describe("Related issue uuid or identifier (ENG-123).")
4505
4669
  });
4506
4670
  server.tool(
4507
4671
  "linear_update_issue",
4508
4672
  'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
4509
4673
  {
4510
- id: z5.string(),
4511
- title: z5.string().optional(),
4512
- description: z5.string().optional(),
4513
- state: z5.string().optional(),
4514
- assignee: z5.string().nullable().optional(),
4674
+ id: z4.string(),
4675
+ title: z4.string().optional(),
4676
+ description: z4.string().optional(),
4677
+ state: z4.string().optional(),
4678
+ assignee: z4.string().nullable().optional(),
4515
4679
  priority: prioritySchema,
4516
- cycle: z5.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
4517
- labels: z5.array(z5.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
4518
- project: z5.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
4519
- parent: z5.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
4520
- relations: z5.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
4521
- removeRelations: z5.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
4680
+ cycle: z4.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
4681
+ labels: z4.array(z4.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
4682
+ project: z4.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
4683
+ parent: z4.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
4684
+ relations: z4.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
4685
+ removeRelations: z4.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
4522
4686
  },
4523
4687
  async (args) => {
4524
4688
  try {
@@ -4532,8 +4696,8 @@ function registerLinearTools(server) {
4532
4696
  "linear_comment",
4533
4697
  "Add a markdown comment on a Linear issue (uuid or ENG-123). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
4534
4698
  {
4535
- id: z5.string(),
4536
- body: z5.string()
4699
+ id: z4.string(),
4700
+ body: z4.string()
4537
4701
  },
4538
4702
  async (args) => {
4539
4703
  try {
@@ -4545,109 +4709,11 @@ function registerLinearTools(server) {
4545
4709
  );
4546
4710
  }
4547
4711
 
4548
- // src/mcp/issue-vendor-tools.ts
4549
- function registerConnectedIssueVendorTools(server, settings = loadAppSettings()) {
4550
- registerGithubIssueTools(server);
4551
- if (isLinearConnected(settings)) registerLinearTools(server);
4552
- if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
4553
- }
4554
-
4555
- // src/mcp/viewer-context-tools.ts
4556
- import { z as z6 } from "zod";
4557
- function text4(payload, isError = false) {
4558
- return mcpJson(payload, isError);
4559
- }
4560
- function matchRegisteredWorkspace(repoPath) {
4561
- const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
4562
- if (!hint) return null;
4563
- let best = null;
4564
- for (const w of listWorkspaces()) {
4565
- const stored = w.path.replace(/\/+$/, "");
4566
- if (stored === hint || hint.startsWith(`${stored}/`)) {
4567
- if (!best || stored.length > best.length) best = stored;
4568
- }
4569
- }
4570
- return best;
4571
- }
4572
- async function resolveProjectRepoPath(repoPath) {
4573
- const hint = repoPath?.trim() || process.cwd();
4574
- let root = hint;
4575
- try {
4576
- root = await resolveRepoRoot(hint);
4577
- } catch {
4578
- root = hint.replace(/\/+$/, "");
4579
- }
4580
- return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
4581
- }
4582
- var PROJECT_PATH_HINT = "Pass repoPath from list_projects. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
4583
- function registerViewerContextTools(server) {
4584
- server.tool(
4585
- "get_viewer_context",
4586
- "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).",
4587
- {
4588
- repoPath: z6.string().optional().describe(
4589
- "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
4590
- )
4591
- },
4592
- async ({ repoPath }) => {
4593
- try {
4594
- const path2 = await resolveProjectRepoPath(repoPath);
4595
- return text4(readViewerContext(path2 ?? void 0));
4596
- } catch (err) {
4597
- return text4(
4598
- { error: err instanceof Error ? err.message : String(err) },
4599
- true
4600
- );
4601
- }
4602
- }
4603
- );
4604
- server.tool(
4605
- "update_viewer_context",
4606
- "Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
4607
- {
4608
- scope: z6.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
4609
- context: z6.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
4610
- confirmed: z6.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
4611
- repoPath: z6.string().optional().describe(
4612
- "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
4613
- )
4614
- },
4615
- async ({ scope, context, confirmed, repoPath }) => {
4616
- try {
4617
- if (scope === "project") {
4618
- const path2 = await resolveProjectRepoPath(repoPath);
4619
- if (!path2) {
4620
- return text4(
4621
- {
4622
- ok: false,
4623
- error: "repoPath required",
4624
- message: PROJECT_PATH_HINT
4625
- },
4626
- true
4627
- );
4628
- }
4629
- const result2 = writeViewerContext({
4630
- scope,
4631
- context,
4632
- repoPath: path2,
4633
- confirmed: confirmed === true
4634
- });
4635
- return text4(result2, !result2.ok);
4636
- }
4637
- const result = writeViewerContext({
4638
- scope,
4639
- context,
4640
- confirmed: confirmed === true
4641
- });
4642
- return text4(result, !result.ok);
4643
- } catch (err) {
4644
- return text4(
4645
- { error: err instanceof Error ? err.message : String(err) },
4646
- true
4647
- );
4648
- }
4649
- }
4650
- );
4712
+ // src/mcp/issue-vendor-tools.ts
4713
+ function registerConnectedIssueVendorTools(server, settings = loadAppSettings()) {
4714
+ registerGithubIssueTools(server);
4715
+ if (isLinearConnected(settings)) registerLinearTools(server);
4716
+ if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
4651
4717
  }
4652
4718
 
4653
4719
  // src/mcp/pr-list.ts
@@ -4685,12 +4751,12 @@ function formatMcpPrList(input) {
4685
4751
  }
4686
4752
 
4687
4753
  // src/mcp/schedule-tools.ts
4688
- import { z as z7 } from "zod";
4689
- function text5(payload, isError = false) {
4754
+ import { z as z5 } from "zod";
4755
+ function text4(payload, isError = false) {
4690
4756
  return mcpJson(payload, isError);
4691
4757
  }
4692
4758
  function fail4(err) {
4693
- return text5(
4759
+ return text4(
4694
4760
  { error: err instanceof Error ? err.message : String(err) },
4695
4761
  true
4696
4762
  );
@@ -4715,7 +4781,7 @@ function registerScheduleTools(server) {
4715
4781
  {},
4716
4782
  async () => {
4717
4783
  try {
4718
- return text5({ schedules: listSchedules() });
4784
+ return text4({ schedules: listSchedules() });
4719
4785
  } catch (err) {
4720
4786
  return fail4(err);
4721
4787
  }
@@ -4725,18 +4791,18 @@ function registerScheduleTools(server) {
4725
4791
  "create_schedule",
4726
4792
  "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.",
4727
4793
  {
4728
- prompt: z7.string().describe("User message / goal queued when the schedule fires"),
4729
- name: z7.string().optional(),
4730
- at: z7.string().optional().describe("ISO datetime for a one-shot"),
4731
- every: z7.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
4732
- cron: z7.string().optional().describe("5-field cron expression"),
4733
- tz: z7.string().optional().describe("IANA timezone for cron (default: system)"),
4734
- chatId: z7.string().optional().describe(
4794
+ prompt: z5.string().describe("User message / goal queued when the schedule fires"),
4795
+ name: z5.string().optional(),
4796
+ at: z5.string().optional().describe("ISO datetime for a one-shot"),
4797
+ every: z5.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
4798
+ cron: z5.string().optional().describe("5-field cron expression"),
4799
+ tz: z5.string().optional().describe("IANA timezone for cron (default: system)"),
4800
+ chatId: z5.string().optional().describe(
4735
4801
  'Existing chat id (orchestration or workspace), or "self" for this chat. Omit to create a new Global chat on fire.'
4736
4802
  ),
4737
- threadId: z7.string().optional().describe("Legacy alias for chatId."),
4738
- agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
4739
- model: z7.string().optional()
4803
+ threadId: z5.string().optional().describe("Legacy alias for chatId."),
4804
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
4805
+ model: z5.string().optional()
4740
4806
  },
4741
4807
  async (args) => {
4742
4808
  try {
@@ -4759,7 +4825,7 @@ function registerScheduleTools(server) {
4759
4825
  model: args.model,
4760
4826
  createdBy: "mcp"
4761
4827
  });
4762
- return text5({
4828
+ return text4({
4763
4829
  schedule,
4764
4830
  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."
4765
4831
  });
@@ -4772,20 +4838,20 @@ function registerScheduleTools(server) {
4772
4838
  "update_schedule",
4773
4839
  "Update a local schedule (prompt, cadence, target chat, enabled). Pass id from list_schedules.",
4774
4840
  {
4775
- id: z7.string(),
4776
- prompt: z7.string().optional(),
4777
- name: z7.string().optional(),
4778
- at: z7.string().optional(),
4779
- every: z7.string().optional(),
4780
- cron: z7.string().optional(),
4781
- tz: z7.string().optional(),
4782
- chatId: z7.string().optional().describe(
4841
+ id: z5.string(),
4842
+ prompt: z5.string().optional(),
4843
+ name: z5.string().optional(),
4844
+ at: z5.string().optional(),
4845
+ every: z5.string().optional(),
4846
+ cron: z5.string().optional(),
4847
+ tz: z5.string().optional(),
4848
+ chatId: z5.string().optional().describe(
4783
4849
  'Existing chat (orchestration or workspace), "self", or empty string to create a new Global chat each run'
4784
4850
  ),
4785
- threadId: z7.string().optional().describe("Legacy alias for chatId."),
4786
- agent: z7.enum(["claude", "cursor", "codex", "opencode"]).optional(),
4787
- model: z7.string().optional(),
4788
- enabled: z7.boolean().optional()
4851
+ threadId: z5.string().optional().describe("Legacy alias for chatId."),
4852
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional(),
4853
+ model: z5.string().optional(),
4854
+ enabled: z5.boolean().optional()
4789
4855
  },
4790
4856
  async (args) => {
4791
4857
  try {
@@ -4817,7 +4883,7 @@ function registerScheduleTools(server) {
4817
4883
  enabled: args.enabled,
4818
4884
  model: args.model
4819
4885
  });
4820
- return text5({ schedule });
4886
+ return text4({ schedule });
4821
4887
  } catch (err) {
4822
4888
  return fail4(err);
4823
4889
  }
@@ -4826,11 +4892,11 @@ function registerScheduleTools(server) {
4826
4892
  server.tool(
4827
4893
  "delete_schedule",
4828
4894
  "Delete a local schedule. Pass id from list_schedules.",
4829
- { id: z7.string() },
4895
+ { id: z5.string() },
4830
4896
  async ({ id }) => {
4831
4897
  try {
4832
4898
  deleteSchedule(id);
4833
- return text5({ ok: true, id });
4899
+ return text4({ ok: true, id });
4834
4900
  } catch (err) {
4835
4901
  return fail4(err);
4836
4902
  }
@@ -4839,14 +4905,14 @@ function registerScheduleTools(server) {
4839
4905
  server.tool(
4840
4906
  "run_schedule",
4841
4907
  "Fire a schedule now (does not wait for nextRunAt). Queues the prompt on the target chat or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
4842
- { id: z7.string() },
4908
+ { id: z5.string() },
4843
4909
  async ({ id }) => {
4844
4910
  try {
4845
4911
  if (!getSchedule(id)) {
4846
4912
  return fail4(new Error(`Schedule not found: ${id}`));
4847
4913
  }
4848
4914
  const schedule = await fireSchedule(id);
4849
- return text5({ schedule });
4915
+ return text4({ schedule });
4850
4916
  } catch (err) {
4851
4917
  return fail4(err);
4852
4918
  }
@@ -4949,7 +5015,7 @@ async function handleJobStop(params, _ctx) {
4949
5015
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
4950
5016
  }
4951
5017
  async function defaultTurnWait(ref, timeoutMs) {
4952
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-XEKODYDQ.js");
5018
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-FPGVKEID.js");
4953
5019
  const orch = getOrchestrator2();
4954
5020
  const thread = await orch.waitForTurn(ref, timeoutMs, {
4955
5021
  resolveIfStillRunning: true
@@ -5286,154 +5352,7 @@ async function getHomeBoardInputs(workspaces, opts) {
5286
5352
  }
5287
5353
  }
5288
5354
 
5289
- // src/mcp/create-thread-agent.ts
5290
- function resolveOrchCreateThreadOptions(input) {
5291
- const { resolveNewThreadOptions: resolveNewThreadOptions2 } = input;
5292
- const account = resolveNewThreadOptions2({});
5293
- const requested = input.requestedAgent;
5294
- const parent = input.parentAgent ?? void 0;
5295
- const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
5296
- let agent = account.agent;
5297
- let ignoredAgent;
5298
- let honorRequestedModel = false;
5299
- if (requested) {
5300
- const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
5301
- const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
5302
- if (parentEcho || cursorAutofill) {
5303
- ignoredAgent = requested;
5304
- } else {
5305
- agent = requested;
5306
- honorRequestedModel = input.requestedModel !== void 0;
5307
- }
5308
- }
5309
- let coercedFrom;
5310
- if (agent === "codex" && parent === "codex" && cursorReady) {
5311
- coercedFrom = requested ?? "codex";
5312
- agent = "cursor";
5313
- }
5314
- const opts = resolveNewThreadOptions2({
5315
- agent,
5316
- model: honorRequestedModel ? input.requestedModel : void 0
5317
- });
5318
- return {
5319
- ...opts,
5320
- ...ignoredAgent ? { ignoredAgent } : {},
5321
- ...coercedFrom ? { coercedFrom } : {}
5322
- };
5323
- }
5324
- function orchCreateThreadAgentNote(input) {
5325
- if (input.coercedFrom) {
5326
- return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
5327
- }
5328
- if (input.ignoredAgent) {
5329
- return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
5330
- }
5331
- return void 0;
5332
- }
5333
-
5334
5355
  // src/mcp/server.ts
5335
- var MAX_ORCH_THREADS = 5;
5336
- var CREATE_THREAD_TIMEOUT_MS = 9e4;
5337
- function withTimeout(promise, ms, label) {
5338
- return new Promise((resolve, reject) => {
5339
- const timer = setTimeout(() => {
5340
- reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
5341
- }, ms);
5342
- promise.then(
5343
- (value) => {
5344
- clearTimeout(timer);
5345
- resolve(value);
5346
- },
5347
- (err) => {
5348
- clearTimeout(timer);
5349
- reject(err);
5350
- }
5351
- );
5352
- });
5353
- }
5354
- async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
5355
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
5356
- let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
5357
- let parent = parentId ? orch.getThread(parentId) : null;
5358
- let parentCorrectedFrom;
5359
- if (envParentId) {
5360
- const envParent = orch.getThread(envParentId);
5361
- if (envParent) {
5362
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
5363
- else if (!parentId) parentCorrectedFrom = void 0;
5364
- parentId = envParentId;
5365
- parent = envParent;
5366
- }
5367
- }
5368
- if (parentId && !parent) {
5369
- parentCorrectedFrom = parentId;
5370
- parentId = "";
5371
- parent = null;
5372
- }
5373
- if (parentId) {
5374
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
5375
- if (children.length >= MAX_ORCH_THREADS) {
5376
- return {
5377
- ok: false,
5378
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
5379
- };
5380
- }
5381
- }
5382
- const opts = resolveOrchCreateThreadOptions({
5383
- requestedAgent: args.agent,
5384
- requestedModel: args.model,
5385
- parentAgent: parent?.agent,
5386
- resolveNewThreadOptions: resolveNewThreadOptions2
5387
- });
5388
- const agentNote = orchCreateThreadAgentNote(opts);
5389
- try {
5390
- const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
5391
- const thread = await withTimeout(
5392
- orch.createThread({
5393
- sourceType: args.sourceType,
5394
- sourceRef: args.sourceRef,
5395
- agent: opts.agent,
5396
- model: opts.model,
5397
- effort: opts.effort,
5398
- fast: opts.fast,
5399
- repoPath: args.repoPath,
5400
- title: args.title,
5401
- parentThreadId: parentId || null,
5402
- cowboy: args.cowboy || void 0,
5403
- attachments: args.attachments
5404
- }),
5405
- CREATE_THREAD_TIMEOUT_MS,
5406
- "create_workspace"
5407
- );
5408
- const alreadyStarted = priorIds.has(thread.id);
5409
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
5410
- return {
5411
- ok: true,
5412
- text: JSON.stringify({
5413
- id: thread.id,
5414
- title: thread.title,
5415
- branchName: thread.branchName,
5416
- worktreePath: thread.worktreePath,
5417
- agent: thread.agent,
5418
- model: thread.model,
5419
- status: thread.status,
5420
- cowboy: Boolean(thread.cowboy),
5421
- link: `sideboard://chat/${thread.id}`,
5422
- parentChatId: thread.parentThreadId,
5423
- parentThreadId: thread.parentThreadId,
5424
- chats: workspaceAgentChatSummaries(thread.worktreePath),
5425
- ...alreadyStarted ? { alreadyStarted: true } : {},
5426
- ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
5427
- ...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
5428
- ...agentNote ? { note: agentNote } : {},
5429
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
5430
- })
5431
- };
5432
- } catch (err) {
5433
- const message = err instanceof Error ? err.message : String(err);
5434
- return { ok: false, text: `create_workspace failed: ${message}` };
5435
- }
5436
- }
5437
5356
  function previewHint(preview) {
5438
5357
  return preview === "window" ? "Electron: the native window is the app. url is the renderer/HMR origin only \u2014 do not open it in a browser (no preload/IPC)." : "Open url in a browser, curl, or Playwright to use the app.";
5439
5358
  }
@@ -5531,16 +5450,16 @@ async function startMcpServer() {
5531
5450
  "list_board",
5532
5451
  "Home Kanban of workspaces (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chats on that card are separate agents. 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 checkout if that ticket/PR/named branch is already checked out. That reuse is one checkout, not one agent \u2014 fork_chat to add another.",
5533
5452
  {
5534
- query: z8.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
5535
- repoPath: z8.string().optional().describe("Limit to one project path from list_projects"),
5536
- kind: z8.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
5537
- ownership: z8.enum(["all", "mine", "reviewing"]).optional().describe(
5453
+ query: z6.string().optional().describe("Case-insensitive token search across title, id, labels, workspace tags, repo"),
5454
+ repoPath: z6.string().optional().describe("Limit to one project path from list_projects"),
5455
+ kind: z6.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
5456
+ ownership: z6.enum(["all", "mine", "reviewing"]).optional().describe(
5538
5457
  "Mine = PRs you authored or local WIP; reviewing = someone else's PR. Uses the gh login."
5539
5458
  ),
5540
- column: z8.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
5459
+ column: z6.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
5541
5460
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
5542
5461
  ),
5543
- limit: z8.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
5462
+ limit: z6.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
5544
5463
  },
5545
5464
  async ({ query, repoPath, kind, ownership, column, limit }) => {
5546
5465
  const workspaces = orch.listWorkspaces();
@@ -5570,7 +5489,7 @@ async function startMcpServer() {
5570
5489
  server.tool(
5571
5490
  "get_chat",
5572
5491
  "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. blockedReason is why that agent is waiting on a person (question, plan approval, or a reported block).",
5573
- { ref: z8.string() },
5492
+ { ref: z6.string() },
5574
5493
  async ({ ref }) => {
5575
5494
  const t = orch.getThread(ref);
5576
5495
  if (!t) {
@@ -5592,6 +5511,7 @@ async function startMcpServer() {
5592
5511
  sessionId: t.sessionId,
5593
5512
  parentChatId: t.parentThreadId,
5594
5513
  parentThreadId: t.parentThreadId,
5514
+ tags: t.tags ?? [],
5595
5515
  children: childThreadRefs(t.id, orch.getThreads(false)),
5596
5516
  queueLength: t.queue.length,
5597
5517
  messageCount: t.messages.length,
@@ -5616,17 +5536,17 @@ async function startMcpServer() {
5616
5536
  "present_artifact",
5617
5537
  "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.",
5618
5538
  {
5619
- title: z8.string().describe("Short title shown in the artifact pane header"),
5620
- type: z8.enum(["html", "svg", "markdown", "react", "log"]).describe(
5539
+ title: z6.string().describe("Short title shown in the artifact pane header"),
5540
+ type: z6.enum(["html", "svg", "markdown", "react", "log"]).describe(
5621
5541
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
5622
5542
  ),
5623
- content: z8.string().describe(
5543
+ content: z6.string().describe(
5624
5544
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
5625
5545
  ),
5626
- artifact_id: z8.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
5627
- status: z8.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
5628
- phase: z8.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
5629
- mode: z8.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
5546
+ artifact_id: z6.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
5547
+ status: z6.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
5548
+ phase: z6.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
5549
+ mode: z6.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
5630
5550
  },
5631
5551
  async ({ title, type, artifact_id, status, phase, mode }) => {
5632
5552
  return mcpJson(
@@ -5639,15 +5559,15 @@ async function startMcpServer() {
5639
5559
  "ask_user",
5640
5560
  "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.",
5641
5561
  {
5642
- questions: z8.array(
5643
- z8.object({
5644
- question: z8.string().describe("Full question text ending with ?"),
5645
- header: z8.string().max(24).optional().describe("Short label shown above the question"),
5646
- multiSelect: z8.boolean().optional().describe("Allow selecting multiple options"),
5647
- options: z8.array(
5648
- z8.object({
5649
- label: z8.string(),
5650
- description: z8.string().optional().describe("What this option means / when to choose it (strongly preferred)")
5562
+ questions: z6.array(
5563
+ z6.object({
5564
+ question: z6.string().describe("Full question text ending with ?"),
5565
+ header: z6.string().max(24).optional().describe("Short label shown above the question"),
5566
+ multiSelect: z6.boolean().optional().describe("Allow selecting multiple options"),
5567
+ options: z6.array(
5568
+ z6.object({
5569
+ label: z6.string(),
5570
+ description: z6.string().optional().describe("What this option means / when to choose it (strongly preferred)")
5651
5571
  })
5652
5572
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
5653
5573
  })
@@ -5673,9 +5593,9 @@ async function startMcpServer() {
5673
5593
  "present_plan",
5674
5594
  PRESENT_PLAN_TOOL_DESCRIPTION,
5675
5595
  {
5676
- title: z8.string().optional().describe("Short plan title (defaults to Plan)"),
5677
- content: z8.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
5678
- thread_id: z8.string().optional().describe("Sideboard thread id when cwd is not the worktree")
5596
+ title: z6.string().optional().describe("Short plan title (defaults to Plan)"),
5597
+ content: z6.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
5598
+ thread_id: z6.string().optional().describe("Sideboard thread id when cwd is not the worktree")
5679
5599
  },
5680
5600
  async ({ title, content, thread_id }) => {
5681
5601
  const { writePlanFile: writePlanFile2 } = await import("./plan-file-7MMIPV7Y.js");
@@ -5704,15 +5624,15 @@ async function startMcpServer() {
5704
5624
  "present_schema",
5705
5625
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
5706
5626
  {
5707
- title: z8.string().describe("Pane title"),
5708
- mode: z8.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
5709
- datasource: z8.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
5710
- resource_id: z8.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
5711
- record_id: z8.string().optional().describe("Record id when opening form mode"),
5712
- resource: z8.record(z8.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
5713
- record: z8.record(z8.unknown()).optional().describe("Inline record { id, data, published_at? }"),
5714
- records: z8.array(z8.record(z8.unknown())).optional().describe("Inline records for table mode"),
5715
- pane_id: z8.string().optional().describe("Stable pane id across updates")
5627
+ title: z6.string().describe("Pane title"),
5628
+ mode: z6.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
5629
+ datasource: z6.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
5630
+ resource_id: z6.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
5631
+ record_id: z6.string().optional().describe("Record id when opening form mode"),
5632
+ resource: z6.record(z6.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
5633
+ record: z6.record(z6.unknown()).optional().describe("Inline record { id, data, published_at? }"),
5634
+ records: z6.array(z6.record(z6.unknown())).optional().describe("Inline records for table mode"),
5635
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
5716
5636
  },
5717
5637
  async (args) => {
5718
5638
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5733,10 +5653,10 @@ async function startMcpServer() {
5733
5653
  "present_files",
5734
5654
  "Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
5735
5655
  {
5736
- title: z8.string().optional().describe("Pane title (default: Files)"),
5737
- datasource: z8.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
5738
- path: z8.string().optional().describe("Initial folder path (e.g. public)"),
5739
- pane_id: z8.string().optional().describe("Stable pane id across updates")
5656
+ title: z6.string().optional().describe("Pane title (default: Files)"),
5657
+ datasource: z6.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
5658
+ path: z6.string().optional().describe("Initial folder path (e.g. public)"),
5659
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
5740
5660
  },
5741
5661
  async (args) => {
5742
5662
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5756,8 +5676,8 @@ async function startMcpServer() {
5756
5676
  "wait_for_job",
5757
5677
  "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.",
5758
5678
  {
5759
- id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
5760
- timeoutMs: z8.number().optional()
5679
+ id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
5680
+ timeoutMs: z6.number().optional()
5761
5681
  },
5762
5682
  async ({ id, timeoutMs }) => {
5763
5683
  const result = await waitForDetachedJob(process.cwd(), id, {
@@ -5770,8 +5690,8 @@ async function startMcpServer() {
5770
5690
  "stop_job",
5771
5691
  "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.",
5772
5692
  {
5773
- id: z8.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
5774
- reason: z8.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
5693
+ id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
5694
+ reason: z6.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
5775
5695
  },
5776
5696
  async ({ id, reason }) => {
5777
5697
  const result = await stopDetachedJob(process.cwd(), id, { reason });
@@ -5783,7 +5703,7 @@ async function startMcpServer() {
5783
5703
  "list_run_scripts",
5784
5704
  "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).",
5785
5705
  {
5786
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd).")
5706
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd).")
5787
5707
  },
5788
5708
  async ({ ref }) => {
5789
5709
  try {
@@ -5813,8 +5733,8 @@ async function startMcpServer() {
5813
5733
  "run_dev_script",
5814
5734
  "Start a .sideboard/.conductor run script (same as the desktop Dev / play button). Several named scripts can run at once; each gets its own port, and starting one does not stop the others. Logs and Stop appear in the thread UI \u2014 do not detached-job or shell-spawn the same command. Returns {port, ports, url, preview}. preview=url \u2192 open url (browser, curl, Playwright). preview=window \u2192 Electron: the native window is the app; url is renderer/HMR only (no preload/IPC in a browser). The port is allocated SIDEBOARD_PORT, not 3000. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
5815
5735
  {
5816
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5817
- name: z8.string().optional().describe("Script name from list_run_scripts (default script if omitted).")
5736
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5737
+ name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted).")
5818
5738
  },
5819
5739
  async ({ ref, name }) => {
5820
5740
  try {
@@ -5842,8 +5762,8 @@ async function startMcpServer() {
5842
5762
  "stop_dev_script",
5843
5763
  "Stop a run script started with run_dev_script or the desktop Dev button (same as UI Stop). Omit name to stop all scripts on this worktree. Worktree turns may omit ref (uses cwd).",
5844
5764
  {
5845
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5846
- name: z8.string().optional().describe("Script name to stop; omit to stop all active scripts.")
5765
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5766
+ name: z6.string().optional().describe("Script name to stop; omit to stop all active scripts.")
5847
5767
  },
5848
5768
  async ({ ref, name }) => {
5849
5769
  try {
@@ -5860,9 +5780,9 @@ async function startMcpServer() {
5860
5780
  "get_run_log",
5861
5781
  "Read the Run-tab terminal for a worktree script (same stdout/stderr as the desktop Dev pane). Default is a tail \u2014 raise `tail` only if the error is missing. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
5862
5782
  {
5863
- ref: z8.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5864
- name: z8.string().optional().describe("Script name from list_run_scripts (default script if omitted)."),
5865
- tail: z8.number().int().min(1).max(MAX_RUN_LOG_CHARS).optional().describe(
5783
+ ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
5784
+ name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted)."),
5785
+ tail: z6.number().int().min(1).max(MAX_RUN_LOG_CHARS).optional().describe(
5866
5786
  `Max characters to return from the end (default ${DEFAULT_RUN_LOG_TAIL_CHARS}). Do not request the full buffer unless the tail is truncated and you still need more.`
5867
5787
  )
5868
5788
  },
@@ -5900,8 +5820,8 @@ async function startMcpServer() {
5900
5820
  "notify_orchestrator",
5901
5821
  "Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). The message is shown on the sidebar and board until the next user message in this chat. ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
5902
5822
  {
5903
- message: z8.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
5904
- reason: z8.enum(["input-required", "blocked"]).optional().describe(
5823
+ message: z6.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
5824
+ reason: z6.enum(["input-required", "blocked"]).optional().describe(
5905
5825
  "input-required = waiting on the user in this chat; blocked = need the coordinator (default)."
5906
5826
  )
5907
5827
  },
@@ -5927,13 +5847,45 @@ async function startMcpServer() {
5927
5847
  ...result.deduped ? { deduped: true } : {}
5928
5848
  });
5929
5849
  } catch (err) {
5930
- const text6 = err instanceof Error ? err.message : String(err);
5931
- return { content: [{ type: "text", text: text6 }], isError: true };
5850
+ const text5 = err instanceof Error ? err.message : String(err);
5851
+ return { content: [{ type: "text", text: text5 }], isError: true };
5932
5852
  }
5933
5853
  }
5934
5854
  );
5935
5855
  }
5936
5856
  registerScheduleTools(server);
5857
+ server.tool(
5858
+ "set_workspace_tags",
5859
+ worktreeProfile ? "Update tags on this workspace (the checkout you are in). Omit ref to use this chat. mode=replace (default), add, or remove. Short labels \u2014 the same ones the orchestrator used for this task. You cannot retag a different checkout." : "Label a project workspace so related checkouts stay recognizable in the sidebar and on the phone. Pass any chat id on that workspace. Use the same tags on every workspace from one task. mode=replace is the default; add and remove change the set. create_workspace tags= sets them at create time. Not for orchestration chats.",
5860
+ {
5861
+ ref: z6.string().optional().describe(
5862
+ worktreeProfile ? "Omit for this chat. Another chat id only if it is on this same checkout." : "Chat id on the workspace to label."
5863
+ ),
5864
+ tags: z6.union([z6.string(), z6.array(z6.string())]).describe("Labels to set. A string may be comma-separated."),
5865
+ mode: z6.enum(["replace", "add", "remove"]).optional()
5866
+ },
5867
+ async ({ ref, tags, mode }) => {
5868
+ try {
5869
+ const callerId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
5870
+ const target = workspaceTagTargetRef({
5871
+ ownWorktreeOnly: worktreeProfile,
5872
+ caller: callerId ? orch.getThread(callerId) : null,
5873
+ requestedRef: ref,
5874
+ find: (id) => orch.getThread(id)
5875
+ });
5876
+ const thread = applyWorkspaceTags(target, tags, mode ?? "replace");
5877
+ return mcpJson({
5878
+ id: thread.id,
5879
+ worktreePath: thread.worktreePath,
5880
+ tags: thread.tags ?? [],
5881
+ chats: workspaceAgentChatSummaries(thread.worktreePath)
5882
+ });
5883
+ } catch (err) {
5884
+ const message = err instanceof Error ? err.message : String(err);
5885
+ return mcpJson({ ok: false, message }, true);
5886
+ }
5887
+ }
5888
+ );
5937
5889
  if (!worktreeProfile) {
5938
5890
  registerConnectedIssueVendorTools(server);
5939
5891
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-DYJGWK6Y.js");
@@ -5944,7 +5896,7 @@ async function startMcpServer() {
5944
5896
  "set_caffeinate",
5945
5897
  "Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
5946
5898
  {
5947
- enabled: z8.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
5899
+ enabled: z6.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
5948
5900
  },
5949
5901
  async ({ enabled }) => {
5950
5902
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
@@ -5968,25 +5920,28 @@ async function startMcpServer() {
5968
5920
  );
5969
5921
  server.tool(
5970
5922
  "create_workspace",
5971
- `Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. 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).`,
5923
+ `Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. 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). Pass the same tags on every workspace you open for one task (short labels such as "phone-sync") so they still match in the sidebar the next day. Reusing a checkout adds those tags.`,
5972
5924
  {
5973
- sourceType: z8.enum(["branch", "pr", "ticket"]),
5974
- sourceRef: z8.string(),
5975
- agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(
5925
+ sourceType: z6.enum(["branch", "pr", "ticket"]),
5926
+ sourceRef: z6.string(),
5927
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(
5976
5928
  `Usually omit. Sideboard uses Account default agent (${accountDefaults.agent}). Passing your own agent or cursor is ignored.`
5977
5929
  ),
5978
- model: z8.string().nullable().optional().describe(
5930
+ model: z6.string().nullable().optional().describe(
5979
5931
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
5980
5932
  ),
5981
- repoPath: z8.string(),
5982
- title: z8.string().optional(),
5983
- cowboy: z8.boolean().optional().describe(
5933
+ repoPath: z6.string(),
5934
+ title: z6.string().optional(),
5935
+ cowboy: z6.boolean().optional().describe(
5984
5936
  "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."
5985
5937
  ),
5986
- parentChatId: z8.string().optional().describe(
5938
+ parentChatId: z6.string().optional().describe(
5987
5939
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5988
5940
  ),
5989
- parentThreadId: z8.string().optional().describe("Legacy alias for parentChatId.")
5941
+ parentThreadId: z6.string().optional().describe("Legacy alias for parentChatId."),
5942
+ tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
5943
+ "Short labels for this workspace (or a comma-separated string). Use the same tags on every checkout for one task. Added when the checkout already exists."
5944
+ )
5990
5945
  },
5991
5946
  async (args) => {
5992
5947
  const result = await createOrchChildThread(orch, args, resolveNewThreadOptions2);
@@ -6000,12 +5955,15 @@ async function startMcpServer() {
6000
5955
  "start_board_card",
6001
5956
  "Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second checkout when one already matches \u2014 returns that workspace (alreadyStarted) and its chats. A workspace can have many agents. fork_chat to add one; send_to_chat only the chat that should do this job.",
6002
5957
  {
6003
- kind: z8.enum(["ticket", "pr", "branch"]),
6004
- ref: z8.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
6005
- repoPath: z8.string().describe("Project path from list_projects / list_board"),
6006
- title: z8.string().optional()
5958
+ kind: z6.enum(["ticket", "pr", "branch"]),
5959
+ ref: z6.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
5960
+ repoPath: z6.string().describe("Project path from list_projects / list_board"),
5961
+ title: z6.string().optional(),
5962
+ tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
5963
+ "Short labels for this workspace. Same tags on every checkout for one task. Added when the checkout already exists."
5964
+ )
6007
5965
  },
6008
- async ({ kind, ref, repoPath, title }) => {
5966
+ async ({ kind, ref, repoPath, title, tags }) => {
6009
5967
  const root = await resolveRepoRoot(repoPath);
6010
5968
  const existing = findLiveThreadForCreate(
6011
5969
  {
@@ -6020,13 +5978,15 @@ async function startMcpServer() {
6020
5978
  }))
6021
5979
  );
6022
5980
  if (existing) {
5981
+ const tagged = tags === void 0 ? orch.getThread(existing.id) : applyWorkspaceTags(existing.id, tags, "add");
6023
5982
  return mcpJson({
6024
5983
  alreadyStarted: true,
6025
5984
  id: existing.id,
6026
5985
  title: existing.title,
6027
5986
  status: existing.status,
5987
+ tags: tagged?.tags ?? [],
6028
5988
  link: `sideboard://chat/${existing.id}`,
6029
- chats: workspaceAgentChatSummaries(orch.getThread(existing.id)?.worktreePath)
5989
+ chats: workspaceAgentChatSummaries(tagged?.worktreePath)
6030
5990
  });
6031
5991
  }
6032
5992
  const workspaces = orch.listWorkspaces();
@@ -6040,7 +6000,8 @@ async function startMcpServer() {
6040
6000
  sourceType: "branch",
6041
6001
  sourceRef,
6042
6002
  repoPath: root,
6043
- title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
6003
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef),
6004
+ tags
6044
6005
  },
6045
6006
  resolveNewThreadOptions2
6046
6007
  );
@@ -6070,7 +6031,8 @@ async function startMcpServer() {
6070
6031
  sourceRef: ident,
6071
6032
  repoPath: root,
6072
6033
  title: title?.trim() || issue?.title,
6073
- attachments
6034
+ attachments,
6035
+ tags
6074
6036
  },
6075
6037
  resolveNewThreadOptions2
6076
6038
  );
@@ -6087,7 +6049,8 @@ async function startMcpServer() {
6087
6049
  sourceType: "pr",
6088
6050
  sourceRef: number,
6089
6051
  repoPath: root,
6090
- title: title?.trim() || pr?.title
6052
+ title: title?.trim() || pr?.title,
6053
+ tags
6091
6054
  },
6092
6055
  resolveNewThreadOptions2
6093
6056
  );
@@ -6099,11 +6062,11 @@ async function startMcpServer() {
6099
6062
  );
6100
6063
  server.tool(
6101
6064
  "send_to_chat",
6102
- '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.',
6065
+ 'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer: interrupt the in-flight turn and start now, same as the workspace composer). 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.',
6103
6066
  {
6104
- ref: z8.string(),
6105
- prompt: z8.string(),
6106
- force_stop: z8.boolean().optional()
6067
+ ref: z6.string(),
6068
+ prompt: z6.string(),
6069
+ force_stop: z6.boolean().optional()
6107
6070
  },
6108
6071
  async ({ ref, prompt, force_stop }) => {
6109
6072
  if (force_stop) {
@@ -6128,8 +6091,8 @@ async function startMcpServer() {
6128
6091
  "wait_for_turn",
6129
6092
  "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. blockedReason is why the child is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. 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).",
6130
6093
  {
6131
- ref: z8.string(),
6132
- timeoutMs: z8.number().optional()
6094
+ ref: z6.string(),
6095
+ timeoutMs: z6.number().optional()
6133
6096
  },
6134
6097
  async ({ ref, timeoutMs }) => {
6135
6098
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
@@ -6157,7 +6120,7 @@ async function startMcpServer() {
6157
6120
  server.tool(
6158
6121
  "get_turn_result",
6159
6122
  "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) and blockedReason (why the agent is waiting on a person).",
6160
- { ref: z8.string() },
6123
+ { ref: z6.string() },
6161
6124
  async ({ ref }) => {
6162
6125
  const result = orch.getTurnResult(ref);
6163
6126
  const hint = mcpWaitTaskHint(result.taskState, result.status, result.blockedReason);
@@ -6171,12 +6134,13 @@ async function startMcpServer() {
6171
6134
  );
6172
6135
  server.tool(
6173
6136
  "stop_chat",
6174
- "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.",
6137
+ "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. Pass reason \u2014 a short note on why this chat is stopping. It is stored on the thread and kept when the turn unwinds. If you omit it, Sideboard still records that the queue was cleared.",
6175
6138
  {
6176
- ref: z8.string(),
6177
- force: z8.boolean().optional()
6139
+ ref: z6.string(),
6140
+ force: z6.boolean().optional(),
6141
+ reason: z6.string().optional().describe("Why this chat is stopping. Stored on the thread for the next session.")
6178
6142
  },
6179
- async ({ ref, force }) => {
6143
+ async ({ ref, force, reason }) => {
6180
6144
  const t = orch.getThread(ref);
6181
6145
  if (!t) {
6182
6146
  return {
@@ -6186,18 +6150,19 @@ async function startMcpServer() {
6186
6150
  }
6187
6151
  const clearQueue = force !== false;
6188
6152
  const hadQueued = t.queue.length > 0;
6189
- const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
6153
+ const stopped = orch.stop(ref, { clearQueue, notifyParent: false, reason });
6190
6154
  return mcpJson({
6191
6155
  id: stopped.id,
6192
6156
  status: stopped.status,
6193
- clearedQueue: clearQueue && hadQueued
6157
+ clearedQueue: clearQueue && hadQueued,
6158
+ lastError: stopped.lastError ?? null
6194
6159
  });
6195
6160
  }
6196
6161
  );
6197
6162
  server.tool(
6198
6163
  "archive_chat",
6199
6164
  "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.",
6200
- { ref: z8.string() },
6165
+ { ref: z6.string() },
6201
6166
  async ({ ref }) => {
6202
6167
  const t = orch.getThread(ref);
6203
6168
  if (!t) {
@@ -6223,7 +6188,7 @@ async function startMcpServer() {
6223
6188
  server.tool(
6224
6189
  "restore_chat",
6225
6190
  "Restore an archived thread (recreates worktree from branch when needed)",
6226
- { ref: z8.string() },
6191
+ { ref: z6.string() },
6227
6192
  async ({ ref }) => {
6228
6193
  try {
6229
6194
  const restored = await orch.restore(ref);
@@ -6242,8 +6207,8 @@ async function startMcpServer() {
6242
6207
  "get_diff",
6243
6208
  "Compact diff summary (capped hunks, paginated)",
6244
6209
  {
6245
- ref: z8.string(),
6246
- maxFiles: z8.number().optional()
6210
+ ref: z6.string(),
6211
+ maxFiles: z6.number().optional()
6247
6212
  },
6248
6213
  async ({ ref, maxFiles }) => {
6249
6214
  const summary = await orch.diffSummary(ref);
@@ -6256,7 +6221,7 @@ async function startMcpServer() {
6256
6221
  server.tool(
6257
6222
  "get_pr_checks",
6258
6223
  "Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
6259
- { ref: z8.string().describe("Worktree thread id/ref") },
6224
+ { ref: z6.string().describe("Worktree thread id/ref") },
6260
6225
  async ({ ref }) => {
6261
6226
  try {
6262
6227
  const checks = await orch.getPrChecks(ref);
@@ -6270,7 +6235,7 @@ async function startMcpServer() {
6270
6235
  server.tool(
6271
6236
  "request_review",
6272
6237
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). If that worktree has a single unused idle agent (no messages), runs the review there; otherwise opens a new Review chat tab. Attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information in that chat. The review stays there so the user can read it and type next steps \u2014 do not comment on or update the PR or ticket, and do not ask_user after the review, until they ask. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review thread id.',
6273
- { ref: z8.string().describe("Worktree thread id/ref to review") },
6238
+ { ref: z6.string().describe("Worktree thread id/ref to review") },
6274
6239
  async ({ ref }) => {
6275
6240
  try {
6276
6241
  const tab = await orch.requestReview(ref);
@@ -6292,8 +6257,8 @@ async function startMcpServer() {
6292
6257
  "ask_git",
6293
6258
  "Commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same prompts as the desktop git buttons (including Resolve). Always steers the worktree agent; does not push or open the PR locally. Then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). Do not run git or gh from the orchestration cwd.",
6294
6259
  {
6295
- ref: z8.string().describe("Worktree thread id/ref"),
6296
- action: z8.enum(AGENT_GIT_ACTIONS).describe(
6260
+ ref: z6.string().describe("Worktree thread id/ref"),
6261
+ action: z6.enum(AGENT_GIT_ACTIONS).describe(
6297
6262
  "commit-push | create-draft | create-web | resolve-conflicts | ready-for-review | merge"
6298
6263
  )
6299
6264
  },
@@ -6322,7 +6287,7 @@ async function startMcpServer() {
6322
6287
  }
6323
6288
  }
6324
6289
  );
6325
- const agentEnum = z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6290
+ const agentEnum = z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6326
6291
  server.tool(
6327
6292
  "list_models",
6328
6293
  "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.",
@@ -6343,11 +6308,11 @@ async function startMcpServer() {
6343
6308
  "fork_workspace",
6344
6309
  "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.",
6345
6310
  {
6346
- ref: z8.string().describe("Worktree thread id/ref to fork"),
6347
- through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6311
+ ref: z6.string().describe("Worktree thread id/ref to fork"),
6312
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6348
6313
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6349
- model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6350
- title: z8.string().optional()
6314
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6315
+ title: z6.string().optional()
6351
6316
  },
6352
6317
  async ({ ref, through_index, agent, model, title }) => {
6353
6318
  try {
@@ -6381,11 +6346,11 @@ async function startMcpServer() {
6381
6346
  "fork_chat",
6382
6347
  "Add another agent chat on the SAME workspace (same files, Run, and git). Also forks a Global orchestration chat into a new orchestration tab. Pass agent when the new chat should use a different harness. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Use this for a second agent on a checkout (review, another harness, parallel work on the same branch). Do not create_workspace again for that \u2014 it returns the existing checkout. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only when the work needs its own git checkout.",
6383
6348
  {
6384
- ref: z8.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6385
- through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6349
+ ref: z6.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6350
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6386
6351
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6387
- model: z8.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6388
- title: z8.string().optional()
6352
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6353
+ title: z6.string().optional()
6389
6354
  },
6390
6355
  async ({ ref, through_index, agent, model, title }) => {
6391
6356
  try {
@@ -6423,7 +6388,7 @@ async function startMcpServer() {
6423
6388
  server.tool(
6424
6389
  "run_setup",
6425
6390
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
6426
- { ref: z8.string() },
6391
+ { ref: z6.string() },
6427
6392
  async ({ ref }) => {
6428
6393
  const result = await orch.runSetup(ref);
6429
6394
  return mcpJson(result);
@@ -6432,7 +6397,7 @@ async function startMcpServer() {
6432
6397
  server.tool(
6433
6398
  "add_project",
6434
6399
  "Register a git repo as a Sideboard workspace",
6435
- { repoPath: z8.string() },
6400
+ { repoPath: z6.string() },
6436
6401
  async ({ repoPath }) => {
6437
6402
  const ws = await orch.addWorkspace(repoPath);
6438
6403
  return mcpJson(ws);
@@ -6440,10 +6405,10 @@ async function startMcpServer() {
6440
6405
  );
6441
6406
  server.tool(
6442
6407
  "remove_project",
6443
- "Unregister a Sideboard workspace (does not archive threads)",
6444
- { repoPath: z8.string() },
6408
+ "Unregister a Sideboard workspace and archive its chats",
6409
+ { repoPath: z6.string() },
6445
6410
  async ({ repoPath }) => {
6446
- orch.removeWorkspace(repoPath);
6411
+ await orch.removeWorkspace(repoPath);
6447
6412
  return { content: [{ type: "text", text: "ok" }] };
6448
6413
  }
6449
6414
  );
@@ -6451,12 +6416,12 @@ async function startMcpServer() {
6451
6416
  "fanout",
6452
6417
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
6453
6418
  {
6454
- prompt: z8.string(),
6455
- agents: z8.array(z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6456
- repoPath: z8.string(),
6457
- sourceType: z8.enum(["branch", "pr", "ticket"]).optional(),
6458
- sourceRef: z8.string().optional(),
6459
- title: z8.string().optional()
6419
+ prompt: z6.string(),
6420
+ agents: z6.array(z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6421
+ repoPath: z6.string(),
6422
+ sourceType: z6.enum(["branch", "pr", "ticket"]).optional(),
6423
+ sourceRef: z6.string().optional(),
6424
+ title: z6.string().optional()
6460
6425
  },
6461
6426
  async (args) => {
6462
6427
  const threads = await orch.bestOfN(args);
@@ -6474,8 +6439,8 @@ async function startMcpServer() {
6474
6439
  "list_branches",
6475
6440
  "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).",
6476
6441
  {
6477
- repoPath: z8.string(),
6478
- unmergedOnly: z8.boolean().optional()
6442
+ repoPath: z6.string(),
6443
+ unmergedOnly: z6.boolean().optional()
6479
6444
  },
6480
6445
  async ({ repoPath, unmergedOnly }) => {
6481
6446
  const root = await resolveRepoRoot(repoPath);
@@ -6496,19 +6461,19 @@ async function startMcpServer() {
6496
6461
  "list_prs",
6497
6462
  `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.`,
6498
6463
  {
6499
- repoPath: z8.string(),
6500
- query: z8.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
6501
- queue: z8.enum(["review", "mine", "approved", "changes"]).optional().describe(
6464
+ repoPath: z6.string(),
6465
+ query: z6.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
6466
+ queue: z6.enum(["review", "mine", "approved", "changes"]).optional().describe(
6502
6467
  "review = unclaimed ready-for-review inbox (Settings \u2192 Projects label, default eng-review). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes."
6503
6468
  ),
6504
- state: z8.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
6505
- label: z8.string().optional().describe(
6469
+ state: z6.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
6470
+ label: z6.string().optional().describe(
6506
6471
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
6507
6472
  ),
6508
- reviewer: z8.string().optional().describe(
6473
+ reviewer: z6.string().optional().describe(
6509
6474
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
6510
6475
  ),
6511
- limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
6476
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
6512
6477
  },
6513
6478
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
6514
6479
  const root = await resolveRepoRoot(repoPath);
@@ -6541,7 +6506,7 @@ async function startMcpServer() {
6541
6506
  server.tool(
6542
6507
  "get_pr_stack",
6543
6508
  "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 ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
6544
- { ref: z8.string() },
6509
+ { ref: z6.string() },
6545
6510
  async ({ ref }) => {
6546
6511
  const stack = await orch.getPrStack(ref);
6547
6512
  return mcpJson(stack);
@@ -6551,8 +6516,8 @@ async function startMcpServer() {
6551
6516
  "open_pr_stack_layers",
6552
6517
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6553
6518
  {
6554
- ref: z8.string(),
6555
- layer: z8.number().int().positive().optional()
6519
+ ref: z6.string(),
6520
+ layer: z6.number().int().positive().optional()
6556
6521
  },
6557
6522
  async ({ ref, layer }) => {
6558
6523
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -6575,9 +6540,9 @@ async function startMcpServer() {
6575
6540
  "add_stack_layer",
6576
6541
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6577
6542
  {
6578
- ref: z8.string(),
6579
- branchName: z8.string(),
6580
- title: z8.string().optional()
6543
+ ref: z6.string(),
6544
+ branchName: z6.string(),
6545
+ title: z6.string().optional()
6581
6546
  },
6582
6547
  async ({ ref, branchName, title }) => {
6583
6548
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -6595,11 +6560,11 @@ async function startMcpServer() {
6595
6560
  "create_pr_stack",
6596
6561
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6597
6562
  {
6598
- repoPath: z8.string(),
6599
- branches: z8.array(z8.string()).min(1),
6600
- agent: z8.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6601
- base: z8.string().optional(),
6602
- title: z8.string().optional()
6563
+ repoPath: z6.string(),
6564
+ branches: z6.array(z6.string()).min(1),
6565
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6566
+ base: z6.string().optional(),
6567
+ title: z6.string().optional()
6603
6568
  },
6604
6569
  async (args) => {
6605
6570
  const result = await orch.createPrStack({
@@ -6627,10 +6592,10 @@ async function startMcpServer() {
6627
6592
  "list_issues",
6628
6593
  "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.",
6629
6594
  {
6630
- repoPath: z8.string(),
6631
- query: z8.string().optional().describe("Search title, identifier, or description"),
6632
- assignee: z8.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
6633
- limit: z8.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
6595
+ repoPath: z6.string(),
6596
+ query: z6.string().optional().describe("Search title, identifier, or description"),
6597
+ assignee: z6.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
6598
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
6634
6599
  updatedSince: mcpIssueUpdatedSinceSchema
6635
6600
  },
6636
6601
  async ({ repoPath, query, assignee, limit, updatedSince }) => {
@@ -6950,8 +6915,8 @@ var BrightsySideboardApi = class {
6950
6915
  };
6951
6916
  function taskMessageText(task) {
6952
6917
  const parts = task.message?.parts ?? [];
6953
- const text6 = parts.find((p) => p.kind === "text")?.text ?? "";
6954
- return text6.trim();
6918
+ const text5 = parts.find((p) => p.kind === "text")?.text ?? "";
6919
+ return text5.trim();
6955
6920
  }
6956
6921
 
6957
6922
  // src/brightsy/cloud-connect.ts
@@ -7361,15 +7326,15 @@ var SlackOAuthPendingStore = class {
7361
7326
  const id = state.trim();
7362
7327
  if (!id) return null;
7363
7328
  this.sweep();
7364
- const row = this.items.get(id);
7365
- if (!row) return null;
7329
+ const row2 = this.items.get(id);
7330
+ if (!row2) return null;
7366
7331
  this.items.delete(id);
7367
- return row.value;
7332
+ return row2.value;
7368
7333
  }
7369
7334
  sweep() {
7370
7335
  const now = Date.now();
7371
- for (const [key, row] of this.items) {
7372
- if (row.expiresAt <= now) this.items.delete(key);
7336
+ for (const [key, row2] of this.items) {
7337
+ if (row2.expiresAt <= now) this.items.delete(key);
7373
7338
  }
7374
7339
  }
7375
7340
  };
@@ -7568,11 +7533,11 @@ async function startSlackOAuth(opts) {
7568
7533
  import { WebSocket as WsWebSocket } from "ws";
7569
7534
  var BACKOFF_START_MS = 1e3;
7570
7535
  var BACKOFF_MAX_MS = 3e4;
7571
- function stripSlackMentions(text6) {
7572
- return text6.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
7536
+ function stripSlackMentions(text5) {
7537
+ return text5.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
7573
7538
  }
7574
- function isSlackStopCommand(text6) {
7575
- const t = stripSlackMentions(text6).toLowerCase();
7539
+ function isSlackStopCommand(text5) {
7540
+ const t = stripSlackMentions(text5).toLowerCase();
7576
7541
  return t === "stop" || t === "sideboard_force_stop";
7577
7542
  }
7578
7543
  function parseSlackSocketFrame(raw) {
@@ -7594,8 +7559,8 @@ function inboundFromSocketFrame(frame) {
7594
7559
  const ts = event.ts?.trim();
7595
7560
  if (!channelId || !ts) return null;
7596
7561
  const rawText = event.text?.trim() ?? "";
7597
- const text6 = stripSlackMentions(rawText);
7598
- if (!text6) return null;
7562
+ const text5 = stripSlackMentions(rawText);
7563
+ if (!text5) return null;
7599
7564
  const teamId = (frame.payload?.team_id || event.team || "").trim();
7600
7565
  if (!teamId) return null;
7601
7566
  const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
@@ -7607,7 +7572,7 @@ function inboundFromSocketFrame(frame) {
7607
7572
  ts,
7608
7573
  threadTs: event.thread_ts?.trim() || void 0,
7609
7574
  userId: event.user?.trim(),
7610
- text: text6,
7575
+ text: text5,
7611
7576
  kind: isMention ? "mention" : "dm"
7612
7577
  };
7613
7578
  }
@@ -8240,12 +8205,12 @@ function formatSlackInboundPrompt(msg) {
8240
8205
 
8241
8206
  ${msg.text}`;
8242
8207
  }
8243
- function isSlackInboundUserPrompt(text6) {
8244
- return text6.startsWith("Slack DM\n") || text6.startsWith("Slack @mention\n");
8208
+ function isSlackInboundUserPrompt(text5) {
8209
+ return text5.startsWith("Slack DM\n") || text5.startsWith("Slack @mention\n");
8245
8210
  }
8246
- function formatSlackSignedReply(deviceLabel, text6) {
8211
+ function formatSlackSignedReply(deviceLabel, text5) {
8247
8212
  const label = deviceLabel.trim();
8248
- const body = text6.trim();
8213
+ const body = text5.trim();
8249
8214
  if (!body) return body;
8250
8215
  if (!label) return body;
8251
8216
  const head = `${label}:`;
@@ -8254,8 +8219,8 @@ function formatSlackSignedReply(deviceLabel, text6) {
8254
8219
  }
8255
8220
  return `${label}: ${body}`;
8256
8221
  }
8257
- function signForThisMac(text6) {
8258
- return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text6);
8222
+ function signForThisMac(text5) {
8223
+ return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text5);
8259
8224
  }
8260
8225
  function slackReplyThreadTs(msg) {
8261
8226
  if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
@@ -8316,9 +8281,9 @@ function replyStub(target) {
8316
8281
  kind: "dm"
8317
8282
  };
8318
8283
  }
8319
- async function postSlackText(target, text6, opts) {
8284
+ async function postSlackText(target, text5, opts) {
8320
8285
  if (opts.postReply) {
8321
- const result = await opts.postReply(replyStub(target), text6);
8286
+ const result = await opts.postReply(replyStub(target), text5);
8322
8287
  const ts2 = result && typeof result === "object" && typeof result.ts === "string" ? result.ts.trim() : "";
8323
8288
  return ts2 ? { ts: ts2 } : null;
8324
8289
  }
@@ -8328,7 +8293,7 @@ async function postSlackText(target, text6, opts) {
8328
8293
  "chat.postMessage",
8329
8294
  {
8330
8295
  channel: target.channelId,
8331
- text: text6,
8296
+ text: text5,
8332
8297
  thread_ts: target.threadTs
8333
8298
  },
8334
8299
  opts.fetchImpl
@@ -8336,9 +8301,9 @@ async function postSlackText(target, text6, opts) {
8336
8301
  const ts = json.ts?.trim();
8337
8302
  return ts ? { ts } : null;
8338
8303
  }
8339
- async function updateSlackText(target, ts, text6, opts) {
8304
+ async function updateSlackText(target, ts, text5, opts) {
8340
8305
  if (opts.updateReply) {
8341
- await opts.updateReply(replyStub(target), ts, text6);
8306
+ await opts.updateReply(replyStub(target), ts, text5);
8342
8307
  return;
8343
8308
  }
8344
8309
  const token = writeTokenForTeam(target.teamId);
@@ -8348,7 +8313,7 @@ async function updateSlackText(target, ts, text6, opts) {
8348
8313
  {
8349
8314
  channel: target.channelId,
8350
8315
  ts,
8351
- text: text6
8316
+ text: text5
8352
8317
  },
8353
8318
  opts.fetchImpl
8354
8319
  );
@@ -8376,8 +8341,8 @@ function replyTargetOf(msg) {
8376
8341
  threadTs: slackReplyThreadTs(msg)
8377
8342
  };
8378
8343
  }
8379
- async function postSlackReply(msg, text6, opts) {
8380
- await postSlackText(replyTargetOf(msg), signForThisMac(text6), opts);
8344
+ async function postSlackReply(msg, text5, opts) {
8345
+ await postSlackText(replyTargetOf(msg), signForThisMac(text5), opts);
8381
8346
  }
8382
8347
  function startSlackTurnProgress(msg, threadId, opts) {
8383
8348
  const log = opts.onLog ?? (() => void 0);
@@ -8460,10 +8425,10 @@ function startSlackTurnProgress(msg, threadId, opts) {
8460
8425
  }
8461
8426
  });
8462
8427
  },
8463
- finishWith: (text6) => {
8428
+ finishWith: (text5) => {
8464
8429
  stop();
8465
8430
  return run2(async () => {
8466
- const signed = signForThisMac(text6.trim());
8431
+ const signed = signForThisMac(text5.trim());
8467
8432
  if (!signed) {
8468
8433
  if (!postedTs) return;
8469
8434
  const ts = postedTs;
@@ -8491,11 +8456,11 @@ function startSlackTurnProgress(msg, threadId, opts) {
8491
8456
  };
8492
8457
  }
8493
8458
  var lastRelayed = /* @__PURE__ */ new Map();
8494
- function markRelayed(threadId, text6) {
8495
- lastRelayed.set(threadId, `${threadId}:${text6}`);
8459
+ function markRelayed(threadId, text5) {
8460
+ lastRelayed.set(threadId, `${threadId}:${text5}`);
8496
8461
  }
8497
- function alreadyRelayed(threadId, text6) {
8498
- return lastRelayed.get(threadId) === `${threadId}:${text6}`;
8462
+ function alreadyRelayed(threadId, text5) {
8463
+ return lastRelayed.get(threadId) === `${threadId}:${text5}`;
8499
8464
  }
8500
8465
  async function relayCoordinatorReplyToSlack(threadId, opts) {
8501
8466
  const target = getSlackReplyTarget(threadId);
@@ -8504,14 +8469,14 @@ async function relayCoordinatorReplyToSlack(threadId, opts) {
8504
8469
  const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
8505
8470
  if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
8506
8471
  const result = getOrchestrator().getTurnResult(threadId);
8507
- const text6 = formatSlackTurnReply(result);
8508
- if (!text6) return;
8509
- if (alreadyRelayed(threadId, text6)) return;
8510
- markRelayed(threadId, text6);
8472
+ const text5 = formatSlackTurnReply(result);
8473
+ if (!text5) return;
8474
+ if (alreadyRelayed(threadId, text5)) return;
8475
+ markRelayed(threadId, text5);
8511
8476
  const log = opts.onLog ?? (() => void 0);
8512
8477
  try {
8513
- await postSlackText(target, signForThisMac(text6), opts);
8514
- log(`replied ${target.threadTs ?? target.channelId} (${text6.length} chars)`);
8478
+ await postSlackText(target, signForThisMac(text5), opts);
8479
+ log(`replied ${target.threadTs ?? target.channelId} (${text5.length} chars)`);
8515
8480
  } catch (err) {
8516
8481
  lastRelayed.delete(threadId);
8517
8482
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -8779,11 +8744,11 @@ function questions(value) {
8779
8744
  const out = [];
8780
8745
  for (const item of value) {
8781
8746
  if (!item || typeof item !== "object") return null;
8782
- const row = item;
8783
- const question = str2(row.question);
8784
- if (!question || !Array.isArray(row.options)) return null;
8747
+ const row2 = item;
8748
+ const question = str2(row2.question);
8749
+ if (!question || !Array.isArray(row2.options)) return null;
8785
8750
  const options = [];
8786
- for (const opt of row.options) {
8751
+ for (const opt of row2.options) {
8787
8752
  if (!opt || typeof opt !== "object") return null;
8788
8753
  const label = str2(opt.label);
8789
8754
  if (!label) return null;
@@ -8838,9 +8803,9 @@ function parseRemoteClientMessage(raw) {
8838
8803
  return { type: "list_hosts", sessionTokens };
8839
8804
  }
8840
8805
  if (msg.type === "prompt") {
8841
- const text6 = str2(parsed.text);
8842
- if (!text6) return null;
8843
- return { type: "prompt", text: text6 };
8806
+ const text5 = str2(parsed.text);
8807
+ if (!text5) return null;
8808
+ return { type: "prompt", text: text5 };
8844
8809
  }
8845
8810
  if (msg.type === "assistant") {
8846
8811
  return { type: "assistant", text: str2(parsed.text) };
@@ -8875,15 +8840,15 @@ function parseRemoteServerMessage(raw) {
8875
8840
  const hosts = [];
8876
8841
  for (const item of raw2) {
8877
8842
  if (!item || typeof item !== "object") return null;
8878
- const row = item;
8879
- const sessionToken = str2(row.sessionToken);
8880
- if (!sessionToken || typeof row.deviceId !== "string" || typeof row.deviceLabel !== "string") return null;
8881
- if (typeof row.online !== "boolean") return null;
8843
+ const row2 = item;
8844
+ const sessionToken = str2(row2.sessionToken);
8845
+ if (!sessionToken || typeof row2.deviceId !== "string" || typeof row2.deviceLabel !== "string") return null;
8846
+ if (typeof row2.online !== "boolean") return null;
8882
8847
  hosts.push({
8883
8848
  sessionToken,
8884
- deviceId: row.deviceId.trim(),
8885
- deviceLabel: row.deviceLabel.trim(),
8886
- online: row.online
8849
+ deviceId: row2.deviceId.trim(),
8850
+ deviceLabel: row2.deviceLabel.trim(),
8851
+ online: row2.online
8887
8852
  });
8888
8853
  }
8889
8854
  return { type: "hosts", hosts };
@@ -8902,9 +8867,9 @@ function parseRemoteServerMessage(raw) {
8902
8867
  return { type: "pairing_code", code, expiresAt };
8903
8868
  }
8904
8869
  if (type === "inbound") {
8905
- const text6 = str2(parsed.text);
8906
- if (!text6) return null;
8907
- return { type: "inbound", text: text6 };
8870
+ const text5 = str2(parsed.text);
8871
+ if (!text5) return null;
8872
+ return { type: "inbound", text: text5 };
8908
8873
  }
8909
8874
  if (type === "paired") {
8910
8875
  const deviceId = str2(parsed.deviceId);
@@ -9065,8 +9030,8 @@ var RemoteHub = class {
9065
9030
  send2(socket, { type: "error", message: "register this Mac before pairing" });
9066
9031
  return;
9067
9032
  }
9068
- for (const [code2, row] of this.codes) {
9069
- if (row.deviceId === host.deviceId) this.codes.delete(code2);
9033
+ for (const [code2, row2] of this.codes) {
9034
+ if (row2.deviceId === host.deviceId) this.codes.delete(code2);
9070
9035
  }
9071
9036
  const code = newCode();
9072
9037
  const expiresAt = Date.now() + CODE_TTL_MS;
@@ -9074,13 +9039,13 @@ var RemoteHub = class {
9074
9039
  send2(socket, { type: "pairing_code", code, expiresAt });
9075
9040
  }
9076
9041
  pairPhone(socket, code) {
9077
- const row = this.codes.get(code);
9078
- if (!row || row.expiresAt < Date.now()) {
9042
+ const row2 = this.codes.get(code);
9043
+ if (!row2 || row2.expiresAt < Date.now()) {
9079
9044
  this.codes.delete(code);
9080
9045
  send2(socket, { type: "error", message: "pairing code expired" });
9081
9046
  return;
9082
9047
  }
9083
- const host = this.hosts.get(row.deviceId);
9048
+ const host = this.hosts.get(row2.deviceId);
9084
9049
  if (!host) {
9085
9050
  send2(socket, { type: "error", message: "Mac is offline" });
9086
9051
  return;
@@ -9206,26 +9171,26 @@ var IMAGE_MIME = {
9206
9171
  bmp: "image/bmp"
9207
9172
  };
9208
9173
  function phoneWorktreeRelative(worktreePath, raw) {
9209
- let text6 = raw.trim();
9210
- if (!text6) return null;
9211
- if (/^file:\/\//i.test(text6)) {
9174
+ let text5 = raw.trim();
9175
+ if (!text5) return null;
9176
+ if (/^file:\/\//i.test(text5)) {
9212
9177
  try {
9213
- text6 = decodeURIComponent(new URL(text6).pathname);
9178
+ text5 = decodeURIComponent(new URL(text5).pathname);
9214
9179
  } catch {
9215
9180
  return null;
9216
9181
  }
9217
9182
  }
9218
- text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
9219
- if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
9183
+ text5 = (text5.split("#")[0] ?? text5).replace(/\\/g, "/");
9184
+ if (!text5 || text5.includes("\0") || text5.split("/").includes("..")) return null;
9220
9185
  const root = worktreePath.replace(/\/+$/, "");
9221
9186
  if (!root || root === "/") return null;
9222
- if (text6.startsWith("/")) {
9223
- if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
9224
- text6 = text6 === root ? "" : text6.slice(root.length + 1);
9187
+ if (text5.startsWith("/")) {
9188
+ if (text5 !== root && !text5.startsWith(`${root}/`)) return null;
9189
+ text5 = text5 === root ? "" : text5.slice(root.length + 1);
9225
9190
  }
9226
- text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
9227
- if (!text6 || text6 === "." || text6.startsWith("/")) return null;
9228
- return text6;
9191
+ text5 = text5.replace(/^\.\//, "").replace(/\/+$/, "");
9192
+ if (!text5 || text5 === "." || text5.startsWith("/")) return null;
9193
+ return text5;
9229
9194
  }
9230
9195
  function phoneImageMime(relativePath) {
9231
9196
  const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
@@ -9344,9 +9309,9 @@ function isArtifactTool(name) {
9344
9309
  const short = name.split("__").pop() ?? name;
9345
9310
  return /^(present_artifact|create_artifact|update_artifact)$/i.test(short);
9346
9311
  }
9347
- function phoneArtifactText(text6, parts, limit = Number.POSITIVE_INFINITY) {
9312
+ function phoneArtifactText(text5, parts, limit = Number.POSITIVE_INFINITY) {
9348
9313
  const hints = /* @__PURE__ */ new Set();
9349
- const body = text6.replace(new RegExp(FENCE_RE.source, "g"), (full, language, content) => {
9314
+ const body = text5.replace(new RegExp(FENCE_RE.source, "g"), (full, language, content) => {
9350
9315
  const kind = kindFor(language, content);
9351
9316
  if (!kind) return full;
9352
9317
  const hint = artifactHint(content);
@@ -9383,11 +9348,84 @@ function parseOpenArtifact(value, chatId) {
9383
9348
  return { op: "open-artifact", chatId, title, ...hint ? { hint } : {} };
9384
9349
  }
9385
9350
 
9386
- // src/remote/phone-chats.ts
9387
- var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
9351
+ // src/remote/phone-defaults.ts
9352
+ function row(resolved) {
9353
+ return {
9354
+ agent: resolved.agent,
9355
+ model: resolved.model,
9356
+ effort: resolved.effort,
9357
+ fast: resolved.fast
9358
+ };
9359
+ }
9360
+ function phoneAccountDefaults() {
9361
+ return {
9362
+ orchestration: row(resolveOrchestratorDefaults()),
9363
+ worktree: row(resolveThreadDefaults())
9364
+ };
9365
+ }
9366
+
9367
+ // src/remote/phone-stream.ts
9368
+ var PHONE_STREAM_FLUSH_MS = 80;
9369
+ function phoneStreamFrame(parts) {
9370
+ return {
9371
+ text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
9372
+ activity: liveActivitySummary(parts)
9373
+ };
9374
+ }
9375
+ var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
9376
+ function phoneStreamParts(parts, event) {
9377
+ if (SKIPPED.has(event.type)) return null;
9378
+ return applyAgentEvent(parts, event);
9379
+ }
9380
+ function phoneStreamFlushNow(event) {
9381
+ return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
9382
+ }
9383
+
9384
+ // src/remote/phone-transcript.ts
9388
9385
  var PHONE_USER_PREFIX = "Phone\n\n";
9389
9386
  var TRANSCRIPT_LIMIT = 40;
9390
9387
  var MESSAGE_LIMIT = 4e3;
9388
+ function phoneVisibleText(role, text5) {
9389
+ const trimmed = text5.trim();
9390
+ if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
9391
+ return trimmed.slice(PHONE_USER_PREFIX.length).trim();
9392
+ }
9393
+ return trimmed;
9394
+ }
9395
+ function clip(text5, limit) {
9396
+ if (text5.length <= limit) return text5;
9397
+ return `${text5.slice(0, limit - 1)}\u2026`;
9398
+ }
9399
+ function withLiveTail(thread, messages) {
9400
+ if (thread.status !== "running" && thread.status !== "queued") return messages;
9401
+ const text5 = clip(phoneStreamFrame(readTurnLiveParts(thread.id)).text.trim(), MESSAGE_LIMIT);
9402
+ if (!text5) return messages;
9403
+ const last = messages[messages.length - 1];
9404
+ if (last?.role === "agent" && last.text === text5) {
9405
+ return last.streaming ? messages : [...messages.slice(0, -1), { ...last, streaming: true }];
9406
+ }
9407
+ return [...messages, { role: "agent", text: text5, streaming: true }];
9408
+ }
9409
+ function listPhoneMessages(thread) {
9410
+ const out = [];
9411
+ for (const message of thread.messages) {
9412
+ if (message.role !== "user" && message.role !== "agent") continue;
9413
+ const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
9414
+ const visible = [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n");
9415
+ const text5 = message.role === "agent" ? phoneArtifactText(visible, message.parts, MESSAGE_LIMIT) : clip(visible, MESSAGE_LIMIT);
9416
+ if (!text5) continue;
9417
+ out.push({ role: message.role, text: text5 });
9418
+ }
9419
+ const live = withLiveTail(thread, out);
9420
+ for (const prompt of thread.queue) {
9421
+ const text5 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
9422
+ if (text5) live.push({ role: "user", text: text5 });
9423
+ }
9424
+ return live.slice(-TRANSCRIPT_LIMIT);
9425
+ }
9426
+
9427
+ // src/remote/phone-chats.ts
9428
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
9391
9429
  var PREVIEW_LIMIT = 90;
9392
9430
  var HISTORY_PAGE_SIZE = 40;
9393
9431
  var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
@@ -9398,11 +9436,11 @@ var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
9398
9436
  function encodePhoneControl(payload) {
9399
9437
  return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
9400
9438
  }
9401
- function takePhoneControl(text6) {
9402
- if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
9439
+ function takePhoneControl(text5) {
9440
+ if (!text5.startsWith(PHONE_CONTROL_PREFIX)) return null;
9403
9441
  let parsed;
9404
9442
  try {
9405
- parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
9443
+ parsed = JSON.parse(text5.slice(PHONE_CONTROL_PREFIX.length));
9406
9444
  } catch {
9407
9445
  return "invalid";
9408
9446
  }
@@ -9457,10 +9495,10 @@ function parsePhoneControl(value) {
9457
9495
  if (draft === "invalid") return "invalid";
9458
9496
  if (op === "options") return { op: "options", chatId, ...draft };
9459
9497
  if (op === "prompt") {
9460
- const text6 = typeof value.text === "string" ? value.text.trim() : "";
9461
- const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
9498
+ const text5 = typeof value.text === "string" ? value.text.trim() : "";
9499
+ const hasPayload = Boolean(text5 || draft.files?.length || draft.links?.length);
9462
9500
  if (!hasPayload) return "invalid";
9463
- return { op: "prompt", chatId, text: text6, ...draft };
9501
+ return { op: "prompt", chatId, text: text5, ...draft };
9464
9502
  }
9465
9503
  return "invalid";
9466
9504
  }
@@ -9599,17 +9637,6 @@ function parsePhoneCreate(value) {
9599
9637
  }
9600
9638
  return "invalid";
9601
9639
  }
9602
- function phoneVisibleText(role, text6) {
9603
- const trimmed = text6.trim();
9604
- if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
9605
- return trimmed.slice(PHONE_USER_PREFIX.length).trim();
9606
- }
9607
- return trimmed;
9608
- }
9609
- function clip(text6, limit) {
9610
- if (text6.length <= limit) return text6;
9611
- return `${text6.slice(0, limit - 1)}\u2026`;
9612
- }
9613
9640
  function summarize(thread) {
9614
9641
  return {
9615
9642
  id: thread.id,
@@ -9626,22 +9653,6 @@ function previewOf(thread) {
9626
9653
  if (!last) return "";
9627
9654
  return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
9628
9655
  }
9629
- function listPhoneMessages(thread) {
9630
- const out = [];
9631
- for (const message of thread.messages) {
9632
- if (message.role !== "user" && message.role !== "agent") continue;
9633
- const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
9634
- const visible = [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n");
9635
- const text6 = message.role === "agent" ? phoneArtifactText(visible, message.parts, MESSAGE_LIMIT) : clip(visible, MESSAGE_LIMIT);
9636
- if (!text6) continue;
9637
- out.push({ role: message.role, text: text6 });
9638
- }
9639
- for (const prompt of thread.queue) {
9640
- const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
9641
- if (text6) out.push({ role: "user", text: text6 });
9642
- }
9643
- return out.slice(-TRANSCRIPT_LIMIT);
9644
- }
9645
9656
  function repoName(repoPath) {
9646
9657
  const parts = repoPath.replace(/\/$/, "").split("/");
9647
9658
  return parts[parts.length - 1] || repoPath;
@@ -9657,7 +9668,7 @@ function listPhoneSidebar() {
9657
9668
  const threads = listThreads();
9658
9669
  const byRepo = /* @__PURE__ */ new Map();
9659
9670
  for (const thread of threads) {
9660
- if (!isProjectPath(thread.repoPath)) continue;
9671
+ if (!isProjectPath(thread.repoPath) || isRemovedWorkspace(thread.repoPath)) continue;
9661
9672
  const list = byRepo.get(thread.repoPath) ?? [];
9662
9673
  list.push(thread);
9663
9674
  byRepo.set(thread.repoPath, list);
@@ -9669,12 +9680,16 @@ function listPhoneSidebar() {
9669
9680
  const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
9670
9681
  name: repoName(path2),
9671
9682
  path: path2,
9672
- worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
9673
- label: worktreeDisplayLabelForGroup(group),
9674
- chats: group.map(summarize)
9675
- }))
9683
+ worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => {
9684
+ const tags = workspaceTagsFromGroup(group);
9685
+ return {
9686
+ label: worktreeDisplayLabelForGroup(group),
9687
+ chats: group.map(summarize),
9688
+ ...tags.length > 0 ? { tags } : {}
9689
+ };
9690
+ })
9676
9691
  }));
9677
- return { orchestration: listPhoneChats(), projects };
9692
+ return { orchestration: listPhoneChats(), projects, defaults: phoneAccountDefaults() };
9678
9693
  }
9679
9694
  function historyStamp(thread) {
9680
9695
  return thread.archivedAt || thread.updatedAt;
@@ -9936,23 +9951,6 @@ async function listPhoneSources(repoPath, query) {
9936
9951
  };
9937
9952
  }
9938
9953
 
9939
- // src/remote/phone-stream.ts
9940
- var PHONE_STREAM_FLUSH_MS = 80;
9941
- function phoneStreamFrame(parts) {
9942
- return {
9943
- text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
9944
- activity: liveActivitySummary(parts)
9945
- };
9946
- }
9947
- var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
9948
- function phoneStreamParts(parts, event) {
9949
- if (SKIPPED.has(event.type)) return null;
9950
- return applyAgentEvent(parts, event);
9951
- }
9952
- function phoneStreamFlushNow(event) {
9953
- return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
9954
- }
9955
-
9956
9954
  // src/remote/phone-live.ts
9957
9955
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
9958
9956
  var chatChains = /* @__PURE__ */ new Map();
@@ -9987,8 +9985,8 @@ function enqueueChat(chatId, fn) {
9987
9985
  function emitControl(opts, reply) {
9988
9986
  opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
9989
9987
  }
9990
- function watchPhoneStream(opts, chatId, threadId, generation) {
9991
- let parts = [];
9988
+ function watchPhoneStream(opts, chatId, threadId, generation, seed = []) {
9989
+ let parts = seed.slice();
9992
9990
  let timer = null;
9993
9991
  let lastKey = "";
9994
9992
  let closed = false;
@@ -10024,6 +10022,7 @@ ${frame.text}`;
10024
10022
  parts = next;
10025
10023
  schedule(event.event);
10026
10024
  });
10025
+ if (parts.length) flush();
10027
10026
  return () => {
10028
10027
  closed = true;
10029
10028
  if (timer) clearTimeout(timer);
@@ -10097,7 +10096,7 @@ function deliverPhoneTurn(opts, chatId) {
10097
10096
  phoneTurnGen.set(id, generation);
10098
10097
  enqueueChat(id, async () => {
10099
10098
  if (chatGeneration.get(id) !== generation) return;
10100
- const stopStream = watchPhoneStream(opts, id, id, generation);
10099
+ const stopStream = watchPhoneStream(opts, id, id, generation, readTurnLiveParts(id));
10101
10100
  try {
10102
10101
  await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
10103
10102
  } catch (err) {
@@ -10115,13 +10114,13 @@ function deliverPhoneTurn(opts, chatId) {
10115
10114
  }
10116
10115
 
10117
10116
  // src/remote/listen.ts
10118
- function formatRemotePrompt(text6) {
10117
+ function formatRemotePrompt(text5) {
10119
10118
  return `Phone
10120
10119
 
10121
- ${text6.trim()}`;
10120
+ ${text5.trim()}`;
10122
10121
  }
10123
- function isRemoteStopCommand(text6) {
10124
- return text6.trim().toLowerCase() === "stop";
10122
+ function isRemoteStopCommand(text5) {
10123
+ return text5.trim().toLowerCase() === "stop";
10125
10124
  }
10126
10125
  var handleChain2 = Promise.resolve();
10127
10126
  var inboundGeneration = 0;
@@ -10228,7 +10227,7 @@ function handlePhoneControl(cmd, opts) {
10228
10227
  emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
10229
10228
  return;
10230
10229
  }
10231
- void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
10230
+ void transcribe(cmd.audioBase64).then((text5) => emitControl(opts, { op: "dictated", id, text: text5.trim() })).catch((err) => {
10232
10231
  emitControl(opts, { op: "error", message: phoneError(err) });
10233
10232
  });
10234
10233
  return;
@@ -10441,8 +10440,8 @@ function handlePhoneControl(cmd, opts) {
10441
10440
  }
10442
10441
  const live = readPhoneThread(thread.id) ?? thread;
10443
10442
  const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
10444
- const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
10445
- const formatted = formatRemotePrompt(text6);
10443
+ const text5 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
10444
+ const formatted = formatRemotePrompt(text5);
10446
10445
  await getOrchestrator().send(thread.id, formatted, { attachments: staged });
10447
10446
  await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
10448
10447
  } catch (err) {
@@ -10458,9 +10457,9 @@ function handlePhoneControl(cmd, opts) {
10458
10457
  emitChatTurn(opts, chatId, thread.id);
10459
10458
  });
10460
10459
  }
10461
- function handleRemoteInbound(text6, opts) {
10460
+ function handleRemoteInbound(text5, opts) {
10462
10461
  const log = opts.onLog ?? (() => void 0);
10463
- const body = text6.trim();
10462
+ const body = text5.trim();
10464
10463
  const control = takePhoneControl(body);
10465
10464
  if (control) {
10466
10465
  if (control === "invalid") {
@@ -10534,19 +10533,23 @@ function handleRemoteInbound(text6, opts) {
10534
10533
  import { basename as basename3 } from "path";
10535
10534
  import { watch } from "chokidar";
10536
10535
  var PHONE_BOARD_SYNC_MS = 300;
10537
- function chatKey(row) {
10538
- return [row.id, row.title, row.status, row.agent, row.preview].join(" ");
10536
+ function chatKey(row2) {
10537
+ return [row2.id, row2.title, row2.status, row2.agent, row2.preview].join(" ");
10539
10538
  }
10540
10539
  function phoneBoardKey(sidebar) {
10541
10540
  const orchestration = sidebar.orchestration.map(chatKey).join("\n");
10542
10541
  const projects = sidebar.projects.map((project) => {
10543
- const trees = project.worktrees.map((tree) => `${tree.label} ${tree.chats.map(chatKey).join("\n")}`).join("\n");
10542
+ const trees = project.worktrees.map((tree) => `${tree.label} ${(tree.tags ?? []).join(",")} ${tree.chats.map(chatKey).join("\n")}`).join("\n");
10544
10543
  return `${project.path} ${project.name}
10545
10544
  ${trees}`;
10546
10545
  }).join("\n---\n");
10546
+ const defaults = sidebar.defaults;
10547
+ const defaultsKey = defaults ? [defaults.orchestration, defaults.worktree].map((row2) => [row2.agent, row2.model ?? "", row2.effort, row2.fast ? "1" : "0"].join(" ")).join("\n") : "";
10547
10548
  return `${orchestration}
10548
10549
  ===
10549
- ${projects}`;
10550
+ ${projects}
10551
+ ===
10552
+ ${defaultsKey}`;
10550
10553
  }
10551
10554
  function isPhoneBoardEvent(event) {
10552
10555
  return event.type === "status_changed" || event.type === "turn_started" || event.type === "turn_finished" || event.type === "queue_changed" || event.type === "error" || event.type === "context_compacted" || event.type === "quota_failover";
@@ -10568,7 +10571,7 @@ function subscribePhoneBoard(notify) {
10568
10571
  const data = watch(appDataDir(), { ignoreInitial: true, depth: 0 });
10569
10572
  const onData = (changed) => {
10570
10573
  const name = basename3(changed);
10571
- if (name !== "workspaces.json" && name !== "removed-workspaces.json") return;
10574
+ if (name !== "workspaces.json" && name !== "removed-workspaces.json" && name !== "settings.json") return;
10572
10575
  notify();
10573
10576
  };
10574
10577
  data.on("add", onData);
@@ -11003,8 +11006,8 @@ var RelayAccountStore = class {
11003
11006
  verifyCredential(token) {
11004
11007
  const trimmed = token.trim();
11005
11008
  if (!trimmed) return null;
11006
- const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
11007
- return row?.account_id ?? null;
11009
+ const row2 = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
11010
+ return row2?.account_id ?? null;
11008
11011
  }
11009
11012
  /** Drop one Mac credential. The account and its other Macs stay. */
11010
11013
  logout(token) {
@@ -11078,14 +11081,14 @@ var RelayAccountStore = class {
11078
11081
  }
11079
11082
  }
11080
11083
  requireAccount(accountId) {
11081
- const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
11082
- if (!row) throw new Error("That Sideboard account is no longer on the relay.");
11084
+ const row2 = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
11085
+ if (!row2) throw new Error("That Sideboard account is no longer on the relay.");
11083
11086
  }
11084
11087
  accountIdForVerifiedEmail(email) {
11085
- const row = this.db.prepare(
11088
+ const row2 = this.db.prepare(
11086
11089
  "SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
11087
11090
  ).get(email);
11088
- return row?.account_id;
11091
+ return row2?.account_id;
11089
11092
  }
11090
11093
  insertIdentity(issuer, subject, accountId, email, verified) {
11091
11094
  this.db.prepare(
@@ -11116,11 +11119,11 @@ var RelayLoginSessions = class {
11116
11119
  }
11117
11120
  takeLogin(state) {
11118
11121
  this.sweep();
11119
- const row = this.waiting.get(state);
11120
- if (!row) return null;
11122
+ const row2 = this.waiting.get(state);
11123
+ if (!row2) return null;
11121
11124
  this.waiting.delete(state);
11122
- if (row.expiresAt < Date.now()) return null;
11123
- return row;
11125
+ if (row2.expiresAt < Date.now()) return null;
11126
+ return row2;
11124
11127
  }
11125
11128
  putResult(state, value) {
11126
11129
  this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
@@ -11128,19 +11131,19 @@ var RelayLoginSessions = class {
11128
11131
  /** `undefined` means the browser flow has not finished. */
11129
11132
  takeResult(state) {
11130
11133
  this.sweep();
11131
- const row = this.results.get(state);
11132
- if (!row) return this.waiting.has(state) ? void 0 : null;
11134
+ const row2 = this.results.get(state);
11135
+ if (!row2) return this.waiting.has(state) ? void 0 : null;
11133
11136
  this.results.delete(state);
11134
- if (row.expiresAt < Date.now()) return null;
11135
- return row.value;
11137
+ if (row2.expiresAt < Date.now()) return null;
11138
+ return row2.value;
11136
11139
  }
11137
11140
  sweep() {
11138
11141
  const now = Date.now();
11139
- for (const [state, row] of this.waiting) {
11140
- if (row.expiresAt < now) this.waiting.delete(state);
11142
+ for (const [state, row2] of this.waiting) {
11143
+ if (row2.expiresAt < now) this.waiting.delete(state);
11141
11144
  }
11142
- for (const [state, row] of this.results) {
11143
- if (row.expiresAt < now) this.results.delete(state);
11145
+ for (const [state, row2] of this.results) {
11146
+ if (row2.expiresAt < now) this.results.delete(state);
11144
11147
  }
11145
11148
  }
11146
11149
  };
@@ -11165,10 +11168,10 @@ function relayAuthorizeUrl(provider, client, redirectUri, state) {
11165
11168
  return url.toString();
11166
11169
  }
11167
11170
  async function readJson(res) {
11168
- const text6 = await res.text();
11169
- if (!text6) return {};
11171
+ const text5 = await res.text();
11172
+ if (!text5) return {};
11170
11173
  try {
11171
- return JSON.parse(text6);
11174
+ return JSON.parse(text5);
11172
11175
  } catch {
11173
11176
  throw new Error("Sign-in provider returned a non-JSON response.");
11174
11177
  }
@@ -11204,8 +11207,8 @@ async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
11204
11207
  });
11205
11208
  const emails = await readJson(emailRes);
11206
11209
  const list = Array.isArray(emails) ? emails : [];
11207
- const primary = list.find((row) => row.primary === true && row.verified === true);
11208
- const anyVerified = list.find((row) => row.verified === true);
11210
+ const primary = list.find((row2) => row2.primary === true && row2.verified === true);
11211
+ const anyVerified = list.find((row2) => row2.verified === true);
11209
11212
  const chosen = primary ?? anyVerified;
11210
11213
  const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
11211
11214
  return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
@@ -11222,9 +11225,9 @@ async function readBody(req) {
11222
11225
  if (size > 8192) throw new Error("body too large");
11223
11226
  chunks.push(buf);
11224
11227
  }
11225
- const text6 = Buffer.concat(chunks).toString("utf8").trim();
11226
- if (!text6) return {};
11227
- return JSON.parse(text6);
11228
+ const text5 = Buffer.concat(chunks).toString("utf8").trim();
11229
+ if (!text5) return {};
11230
+ return JSON.parse(text5);
11228
11231
  }
11229
11232
  function sendJson2(res, status, body) {
11230
11233
  res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
@@ -11605,10 +11608,10 @@ async function publishSlackAppHome(opts) {
11605
11608
  }
11606
11609
 
11607
11610
  // src/slack/relay-hub.ts
11608
- function parseSlackDeviceDestination(text6) {
11609
- const m = text6.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
11610
- if (!m) return { label: null, rest: text6 };
11611
- return { label: m[1], rest: text6.slice(m[0].length) };
11611
+ function parseSlackDeviceDestination(text5) {
11612
+ const m = text5.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
11613
+ if (!m) return { label: null, rest: text5 };
11614
+ return { label: m[1], rest: text5.slice(m[0].length) };
11612
11615
  }
11613
11616
  var SlackRelayHub = class {
11614
11617
  sessions = /* @__PURE__ */ new Map();
@@ -12386,8 +12389,8 @@ ${input.permalink}` : "";
12386
12389
 
12387
12390
  ${body}${link}`;
12388
12391
  }
12389
- function isSlackExternalReplyPrompt(text6) {
12390
- return isSlackExternalReplyText(text6);
12392
+ function isSlackExternalReplyPrompt(text5) {
12393
+ return isSlackExternalReplyText(text5);
12391
12394
  }
12392
12395
  function pendingSlackExternalReplies(messages) {
12393
12396
  return pendingInjectedNotices(messages, isSlackExternalReplyText);
@@ -12408,7 +12411,7 @@ async function continueSourceThread(threadId, prompt) {
12408
12411
  await continueOnReply(threadId, prompt);
12409
12412
  return;
12410
12413
  }
12411
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-XEKODYDQ.js");
12414
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-FPGVKEID.js");
12412
12415
  await getOrchestrator2().send(threadId, prompt);
12413
12416
  } catch {
12414
12417
  }
@@ -12416,9 +12419,9 @@ async function continueSourceThread(threadId, prompt) {
12416
12419
  function listSlackOutboundWatches() {
12417
12420
  return pruneWatches(readStore3());
12418
12421
  }
12419
- function formatOwnerSlackFyi(userName, text6) {
12422
+ function formatOwnerSlackFyi(userName, text5) {
12420
12423
  const who = userName.trim() || "Someone";
12421
- const body = text6.trim() || "(no text)";
12424
+ const body = text5.trim() || "(no text)";
12422
12425
  return `${who} replied in Slack:
12423
12426
  ${body}`;
12424
12427
  }
@@ -12433,7 +12436,7 @@ async function relayExternalReply(opts) {
12433
12436
  const thread = readThread(threadId);
12434
12437
  if (!thread || thread.status === "archived") return "ignored";
12435
12438
  try {
12436
- const text6 = formatSlackExternalReplyPrompt({
12439
+ const text5 = formatSlackExternalReplyPrompt({
12437
12440
  userName: opts.reply.userName,
12438
12441
  kind: opts.watch.kind,
12439
12442
  toLabel: opts.watch.toLabel,
@@ -12442,7 +12445,7 @@ async function relayExternalReply(opts) {
12442
12445
  });
12443
12446
  appendMessage(threadId, {
12444
12447
  role: "agent",
12445
- text: text6,
12448
+ text: text5,
12446
12449
  ts: (/* @__PURE__ */ new Date()).toISOString()
12447
12450
  });
12448
12451
  } catch {
@@ -12866,6 +12869,7 @@ export {
12866
12869
  applySystemCaEnv,
12867
12870
  applyThreadIntoMain,
12868
12871
  applyTurnUsage,
12872
+ applyWorkspaceTags,
12869
12873
  armSchedules,
12870
12874
  assertAbleTimeCredential,
12871
12875
  assertAbleTimeMcpCredential,
@@ -13173,6 +13177,7 @@ export {
13173
13177
  gitBranchPrefixSetting,
13174
13178
  githubAgentGitEnv,
13175
13179
  githubHttpsInsteadOfEntries,
13180
+ githubProjectsOnIssue,
13176
13181
  githubRelationEditFlag,
13177
13182
  globalAgentCwd,
13178
13183
  groupHomeBoardWorktrees,
@@ -13259,6 +13264,7 @@ export {
13259
13264
  isPrimaryCheckoutThread,
13260
13265
  isRemoteAccountLoginCancelled,
13261
13266
  isRemoteStopCommand,
13267
+ isRemovedWorkspace,
13262
13268
  isSelfWrittenRecord,
13263
13269
  isSessionQuotaLimit,
13264
13270
  isSetupLastError,
@@ -13315,6 +13321,7 @@ export {
13315
13321
  listCursorModels,
13316
13322
  listGitHubIssueCommentsSince,
13317
13323
  listGitHubIssues,
13324
+ listGitHubProjects,
13318
13325
  listGlobalThreads,
13319
13326
  listIssues,
13320
13327
  listLinearAssignedIssues,
@@ -13322,6 +13329,7 @@ export {
13322
13329
  listLinearIssues,
13323
13330
  listLinearIssuesDirect,
13324
13331
  listLinearIssuesFiltered,
13332
+ listLinearProjects,
13325
13333
  listLinearTeamCycles,
13326
13334
  listLinearTeams,
13327
13335
  listModelsForAgent,
@@ -13393,6 +13401,7 @@ export {
13393
13401
  normalizeThinkingEffort,
13394
13402
  normalizeThread,
13395
13403
  normalizeTurnInput,
13404
+ normalizeWorkspaceTags,
13396
13405
  normalizeWorktreePath,
13397
13406
  notifyOrchestrator,
13398
13407
  npmGlobalNodeModules,
@@ -13554,6 +13563,7 @@ export {
13554
13563
  runSlackListen,
13555
13564
  runSlackRelayClient,
13556
13565
  runWorkspaceSetup,
13566
+ sameWorkspacePath,
13557
13567
  sameWorktreePath,
13558
13568
  sanitizeGitBranchPrefix,
13559
13569
  sanitizeMcpServerName,
@@ -13703,6 +13713,8 @@ export {
13703
13713
  withTurnScopedCost,
13704
13714
  workspaceAgentChatSummaries,
13705
13715
  workspaceSettingsSourceLabel,
13716
+ workspaceTagTargetRef,
13717
+ workspaceTagsFromGroup,
13706
13718
  worktreeAgentBlock,
13707
13719
  worktreeBoardStatus,
13708
13720
  worktreeCleanupSettings,