@sideboard-ai/core 0.1.124 → 0.1.127

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 (39) hide show
  1. package/dist/{agents-DPR2TDNF.js → agents-7BUQY2WX.js} +6 -6
  2. package/dist/{agents-DCXXLTXF.js → agents-X2I3RNCY.js} +6 -6
  3. package/dist/{app-settings-YFWV7MOG.js → app-settings-6RPALX4J.js} +7 -1
  4. package/dist/{app-settings-IKFWZDUK.js → app-settings-E7NEQY7D.js} +7 -1
  5. package/dist/{chunk-SDCPQL27.js → chunk-DLM5LSQW.js} +484 -93
  6. package/dist/{chunk-RG737OWT.js → chunk-G6X6UFJO.js} +2 -0
  7. package/dist/{chunk-HPAWHIZ3.js → chunk-HLJUNJBF.js} +2 -0
  8. package/dist/{chunk-OHEFHIG3.js → chunk-HRSBZIXS.js} +14 -6
  9. package/dist/{chunk-KWGP6RZM.js → chunk-HULESWLI.js} +4 -4
  10. package/dist/{chunk-FXQPY2KU.js → chunk-HUYEU4GS.js} +26 -4
  11. package/dist/{chunk-2X7I6MDW.js → chunk-HZPO3SSZ.js} +11 -8
  12. package/dist/{chunk-O43IRQ2Y.js → chunk-MNL4FSKY.js} +4 -4
  13. package/dist/{chunk-7Y3AYQWT.js → chunk-NV73AO7Q.js} +4 -4
  14. package/dist/{chunk-TRTWH6C2.js → chunk-RSFCB23A.js} +11 -8
  15. package/dist/{chunk-RBVVWBVB.js → chunk-SYLTXDRF.js} +2 -2
  16. package/dist/{chunk-MZDAEI3Y.js → chunk-T7KJQJAX.js} +496 -109
  17. package/dist/{chunk-UAVZ2JSO.js → chunk-UDQI3N47.js} +14 -6
  18. package/dist/{chunk-HZLE6LJJ.js → chunk-VE22NWBD.js} +2 -2
  19. package/dist/{chunk-JNAIKICO.js → chunk-ZMUOPTKZ.js} +4 -4
  20. package/dist/{chunk-BMIRX64H.js → chunk-ZND6XV6J.js} +26 -4
  21. package/dist/{coordinator-prompt-AZ3WRDNC.js → coordinator-prompt-MEEUOEF2.js} +4 -4
  22. package/dist/{coordinator-prompt-LEU62W25.js → coordinator-prompt-SPJSGAQS.js} +4 -4
  23. package/dist/{global-workspace-U5UOVXKQ.js → global-workspace-BMWP7YZC.js} +5 -5
  24. package/dist/{global-workspace-4YNYDMLQ.js → global-workspace-KNESLWKG.js} +5 -5
  25. package/dist/index.cjs +1208 -236
  26. package/dist/index.d.cts +303 -103
  27. package/dist/index.d.ts +303 -103
  28. package/dist/index.js +671 -129
  29. package/dist/mcp/run-stdio.cjs +1054 -194
  30. package/dist/mcp/run-stdio.js +554 -114
  31. package/dist/{orchestrator-VJQ5EWZZ.js → orchestrator-3JLPPTCF.js} +8 -8
  32. package/dist/{orchestrator-JS3EHI4E.js → orchestrator-6GUCE4LY.js} +8 -8
  33. package/dist/{thread-store-JR235AWK.js → thread-store-5MLYY35S.js} +1 -1
  34. package/dist/{thread-store-PMH3BMB6.js → thread-store-F6BCARQG.js} +1 -1
  35. package/dist/{workspaces-ICK433ZV.js → workspaces-HSYRYJYV.js} +6 -6
  36. package/dist/{workspaces-POWKTH2D.js → workspaces-O4QZOASB.js} +6 -6
  37. package/dist/{worktree-CMGBTVN4.js → worktree-ERHC7Q4F.js} +5 -3
  38. package/dist/{worktree-T57RD7BQ.js → worktree-JVGSLSLJ.js} +5 -3
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  AGENT_GIT_ACTIONS,
3
+ BOARD_COLUMN_DEFS,
3
4
  CONTEXT_COMPACT_CHARS,
4
5
  CONTEXT_KEEP_RECENT_CHARS,
5
6
  CONTEXT_KEEP_RECENT_MESSAGES,
6
7
  CONTEXT_MIN_MESSAGES,
8
+ HOME_BOARD_AGENT_HINT,
9
+ HOME_BOARD_CACHE_TTL_MS,
7
10
  LEGACY_REVIEW_REQUEST_PATH,
8
11
  Orchestrator,
9
12
  REPO_REVIEW_NAME,
@@ -25,9 +28,11 @@ import {
25
28
  applyPromptCacheTtlEnv,
26
29
  applyThreadIntoMain,
27
30
  armSchedules,
31
+ assembleHomeBoard,
28
32
  attachmentFromAbsolutePath,
29
33
  attachmentsFromBuffers,
30
34
  attachmentsFromWorktreePaths,
35
+ boardPinIdentity,
31
36
  buildForkTranscriptAttachment,
32
37
  buildReviewRequestAttachment,
33
38
  buildSessionSeed,
@@ -35,6 +40,7 @@ import {
35
40
  captureLoginEnv,
36
41
  captureTurnBaseline,
37
42
  claimDesktopHost,
43
+ classifyWorktreeColumn,
38
44
  cleanupOrphanWorktrees,
39
45
  cloneRepoIntoSideboard,
40
46
  computeNextRunAt,
@@ -46,19 +52,24 @@ import {
46
52
  createPrStack,
47
53
  createSchedule,
48
54
  createThread,
55
+ dedupeBoardIssues,
56
+ dedupeBoardPrs,
49
57
  defaultScheduleName,
50
58
  deleteSchedule,
51
59
  desktopHostPidPath,
52
60
  detectAgents,
53
61
  disconnectSlackWorkspace,
54
62
  discoverSkills,
55
- dismissSlackReplyBadge,
56
63
  ensureReviewRequestFile,
57
64
  ensureReviewSkillFile,
58
65
  estimateMessageChars,
59
66
  estimateThreadChars,
60
67
  expandComposerPrompt,
61
68
  extractiveSummary,
69
+ findBoardIssue,
70
+ findBoardPin,
71
+ findBoardPr,
72
+ findLiveThreadForCreate,
62
73
  findOrphanWorktrees,
63
74
  findThreadForStackLayer,
64
75
  fireSchedule,
@@ -88,6 +99,7 @@ import {
88
99
  getSchedule,
89
100
  getSlackReplyTarget,
90
101
  getSlackWorkspace,
102
+ groupHomeBoardWorktrees,
91
103
  hasEnabledSchedules,
92
104
  importConductorWorkspace,
93
105
  importConductorWorkspaceAsync,
@@ -96,11 +108,13 @@ import {
96
108
  inspectGitWorktree,
97
109
  isCowboyThread,
98
110
  isDesktopHostAlive,
111
+ isHomeBoardThread,
99
112
  isImageFilePath,
100
113
  isPidAlive,
101
114
  isPrimaryCheckoutThread,
102
115
  isSlackExternalReplyPrompt,
103
116
  isThisProcessDesktopHost,
117
+ issueNeedsWorkspacePick,
104
118
  lastRequestOccupancy,
105
119
  listBranchCommits,
106
120
  listConductorWorkspaces,
@@ -108,7 +122,6 @@ import {
108
122
  listRunScripts,
109
123
  listSchedules,
110
124
  listSlackOutboundWatches,
111
- listSlackReplyBadges,
112
125
  listSlackWorkspaces,
113
126
  listSlackWorkspacesRaw,
114
127
  listWorktreeFiles,
@@ -118,7 +131,7 @@ import {
118
131
  openStackLayer,
119
132
  parseDurationMs,
120
133
  pendingSlackExternalReplies,
121
- permalinkForSlackReplyBadge,
134
+ pollSlackOutboundWatches,
122
135
  previewLand,
123
136
  readExistingReviewRequestFile,
124
137
  readSkillBody,
@@ -128,7 +141,6 @@ import {
128
141
  readWorktreeInclude,
129
142
  recordScheduleRun,
130
143
  recordSlackOutboundWatch,
131
- refreshSlackReplyBadges,
132
144
  releaseDesktopHost,
133
145
  requestReview,
134
146
  requireAgent,
@@ -161,6 +173,7 @@ import {
161
173
  startDevServer,
162
174
  startOrchestration,
163
175
  summarizeConversation,
176
+ syncBoardPins,
164
177
  takenTeamSlugsForChatTab,
165
178
  thisProcessShouldDrainAgentQueues,
166
179
  threadsSharingWorktree,
@@ -168,10 +181,11 @@ import {
168
181
  upsertSlackWorkspace,
169
182
  waitForPidExit,
170
183
  withAgentInstructions,
184
+ worktreeBoardStatus,
171
185
  worktreeCleanupSettings,
172
186
  wrapReviewSkillMarkdown,
173
187
  writeWorktreeFile
174
- } from "./chunk-MZDAEI3Y.js";
188
+ } from "./chunk-T7KJQJAX.js";
175
189
  import {
176
190
  BRIGHTSY_MCP_ALLOWED_TOOLS,
177
191
  CLAUDE_MODEL_CATALOG,
@@ -250,7 +264,7 @@ import {
250
264
  withEventParentId,
251
265
  withEventsParentId,
252
266
  writeInjectedMcpConfig
253
- } from "./chunk-7Y3AYQWT.js";
267
+ } from "./chunk-NV73AO7Q.js";
254
268
  import {
255
269
  brightsyConfigPath,
256
270
  brightsyMcpServerName,
@@ -269,7 +283,7 @@ import {
269
283
  listWorkspaces,
270
284
  removeWorkspace,
271
285
  syncWorkspacesFromThreads
272
- } from "./chunk-HZLE6LJJ.js";
286
+ } from "./chunk-VE22NWBD.js";
273
287
  import {
274
288
  CLOUD_COORDINATOR_BUSY_REPLY,
275
289
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -297,7 +311,7 @@ import {
297
311
  parseForceStopMessage,
298
312
  slackCoordinatorSourceRef,
299
313
  takenTeamSlugsForOrchestration
300
- } from "./chunk-KWGP6RZM.js";
314
+ } from "./chunk-HULESWLI.js";
301
315
  import {
302
316
  COORDINATOR_TOOL_PLAYBOOK,
303
317
  SLACK_REPLY_FORMATTING,
@@ -306,7 +320,7 @@ import {
306
320
  enrichWorkspacesWithGithub,
307
321
  ensureGlobalCoordinatorCwd,
308
322
  formatWorkspaceInventory
309
- } from "./chunk-2X7I6MDW.js";
323
+ } from "./chunk-HZPO3SSZ.js";
310
324
  import {
311
325
  LEGACY_PLAN_FILE_REL,
312
326
  PLAN_FILE_NAME,
@@ -343,6 +357,7 @@ import {
343
357
  appendIndexedGitConfig,
344
358
  applyGithubGitAuthEnv,
345
359
  branchDisplayLabel,
360
+ canonicalizeRepoPath,
346
361
  checkoutPrStackLayer,
347
362
  codexSandboxWritableRootsArgs,
348
363
  codexUnattendedGitConfigArgs,
@@ -415,7 +430,7 @@ import {
415
430
  worktreeDisplayLabel,
416
431
  worktreeDisplayLabelForGroup,
417
432
  worktreeNameFromPath
418
- } from "./chunk-UAVZ2JSO.js";
433
+ } from "./chunk-UDQI3N47.js";
419
434
  import {
420
435
  ATTACHMENTS_DIR,
421
436
  LEGACY_ATTACHMENTS_DIR,
@@ -425,6 +440,7 @@ import {
425
440
  import {
426
441
  GITHUB_GIT_AUTH_MODES,
427
442
  HARNESS_ENV_KEYS,
443
+ ISSUE_SOURCE_LABELS,
428
444
  appSettingsPath,
429
445
  applyAppEnvironment,
430
446
  autoArchiveOnMergeEnabled,
@@ -454,7 +470,9 @@ import {
454
470
  getIssueSource,
455
471
  getLinearApiKey,
456
472
  harnessEnvKey,
473
+ isIssueSourceConnected,
457
474
  isLinearConnected,
475
+ issueSourceLabel,
458
476
  loadAppSettings,
459
477
  maxConcurrentAgents,
460
478
  orchestrationQuotaFallbackAgent,
@@ -484,7 +502,7 @@ import {
484
502
  updateDefaultsSettings,
485
503
  updateIntegrationsSettings,
486
504
  updateOpencodeSettings
487
- } from "./chunk-FXQPY2KU.js";
505
+ } from "./chunk-HUYEU4GS.js";
488
506
  import {
489
507
  stripNestedElectronEnv
490
508
  } from "./chunk-4TR3HZFT.js";
@@ -503,7 +521,7 @@ import {
503
521
  updateThread,
504
522
  withThreadLock,
505
523
  writeThread
506
- } from "./chunk-HPAWHIZ3.js";
524
+ } from "./chunk-HLJUNJBF.js";
507
525
  import {
508
526
  THINKING_EFFORTS,
509
527
  isThinkingEffort,
@@ -912,10 +930,13 @@ var ISSUE_FIELDS = `
912
930
  assignee { id name }
913
931
  team { id key name states { nodes { id name type } } }
914
932
  labels { nodes { name } }
933
+ cycle { id name number startsAt endsAt completedAt }
915
934
  `;
916
935
  var ASSIGNED_ISSUES_QUERY = `
917
936
  query SideboardAssignedIssues($first: Int!) {
918
937
  viewer {
938
+ id
939
+ name
919
940
  assignedIssues(
920
941
  first: $first
921
942
  orderBy: updatedAt
@@ -1018,6 +1039,25 @@ function mapState(node) {
1018
1039
  type: String(node.type ?? "")
1019
1040
  };
1020
1041
  }
1042
+ function linearCycleIsActive(cycle, now = Date.now()) {
1043
+ if (!cycle) return false;
1044
+ if (cycle.completedAt) return false;
1045
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
1046
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
1047
+ if (Number.isFinite(start) && now < start) return false;
1048
+ if (Number.isFinite(end) && now > end) return false;
1049
+ return true;
1050
+ }
1051
+ function mapCycle(node) {
1052
+ if (!node?.name && node?.number == null) return null;
1053
+ const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
1054
+ if (!name) return null;
1055
+ return {
1056
+ name,
1057
+ number: typeof node.number === "number" ? node.number : void 0,
1058
+ isActive: linearCycleIsActive(node)
1059
+ };
1060
+ }
1021
1061
  function mapIssue(node) {
1022
1062
  const team = node.team;
1023
1063
  return {
@@ -1035,7 +1075,8 @@ function mapIssue(node) {
1035
1075
  name: String(team.name ?? ""),
1036
1076
  states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
1037
1077
  } : void 0,
1038
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
1078
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
1079
+ cycle: mapCycle(node.cycle)
1039
1080
  };
1040
1081
  }
1041
1082
  function toIssueInfo(issue) {
@@ -1045,7 +1086,11 @@ function toIssueInfo(issue) {
1045
1086
  title: issue.title,
1046
1087
  url: issue.url,
1047
1088
  labels: issue.labels,
1048
- provider: "linear"
1089
+ provider: "linear",
1090
+ assignee: issue.assignee?.name,
1091
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
1092
+ cycle: issue.cycle ?? null,
1093
+ teamKey: issue.team?.key || void 0
1049
1094
  };
1050
1095
  }
1051
1096
  function mapTeam(node) {
@@ -1087,10 +1132,21 @@ function normalizePriority(priority) {
1087
1132
  }
1088
1133
  return priority;
1089
1134
  }
1090
- async function listLinearIssuesDirect(opts) {
1091
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
1135
+ async function listLinearAssignedIssues(opts) {
1136
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
1092
1137
  const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
1093
- return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
1138
+ return {
1139
+ viewer: {
1140
+ id: String(json.viewer?.id ?? ""),
1141
+ name: String(json.viewer?.name ?? "")
1142
+ },
1143
+ issues: (json.viewer?.assignedIssues?.nodes ?? []).map(
1144
+ (node) => toIssueInfo(mapIssue(node))
1145
+ )
1146
+ };
1147
+ }
1148
+ async function listLinearIssuesDirect(opts) {
1149
+ return (await listLinearAssignedIssues(opts)).issues;
1094
1150
  }
1095
1151
  async function listLinearTeams(opts) {
1096
1152
  const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
@@ -1209,13 +1265,13 @@ async function validateLinearApiKey(apiKey) {
1209
1265
 
1210
1266
  // src/integrations/issues.ts
1211
1267
  async function listGitHubIssues(repoPath, opts) {
1212
- const limit = Math.max(1, Math.min(100, opts?.limit ?? 50));
1268
+ const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
1213
1269
  const slug = await resolveGithubRepoSlug(repoPath);
1214
1270
  const args = [
1215
1271
  "issue",
1216
1272
  "list",
1217
1273
  "--json",
1218
- "number,title,url,labels",
1274
+ "number,title,url,labels,assignees",
1219
1275
  "--limit",
1220
1276
  String(limit),
1221
1277
  "--state",
@@ -1238,27 +1294,55 @@ async function listGitHubIssues(repoPath, opts) {
1238
1294
  const labels = (item.labels ?? []).map(
1239
1295
  (l) => typeof l === "string" ? l : String(l?.name ?? "")
1240
1296
  ).filter(Boolean);
1297
+ const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
1241
1298
  return {
1242
1299
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
1243
1300
  identifier,
1244
1301
  title: String(item.title ?? ""),
1245
1302
  url: String(item.url ?? ""),
1246
1303
  labels,
1247
- provider: "github"
1304
+ provider: "github",
1305
+ assignee: assignees[0],
1306
+ assignees
1248
1307
  };
1249
1308
  });
1250
1309
  }
1310
+ async function githubViewerLogin(repoPath) {
1311
+ const { stdout, exitCode } = await gh(["api", "user", "--jq", ".login"], repoPath, {
1312
+ reject: false
1313
+ });
1314
+ if (exitCode !== 0) return "";
1315
+ return stdout.trim();
1316
+ }
1251
1317
  async function listIssues(repoPath) {
1252
1318
  const settings = loadAppSettings();
1253
1319
  const preferredSource = settings.integrations.issueSource ?? "github";
1254
1320
  const linearConnected = isLinearConnected(settings);
1255
1321
  const source = resolveEffectiveIssueSource(settings);
1256
1322
  if (source === "linear") {
1257
- const issues2 = await listLinearIssuesDirect();
1258
- return { source, preferredSource, linearConnected, issues: issues2 };
1323
+ const listed = await listLinearAssignedIssues();
1324
+ return {
1325
+ source,
1326
+ preferredSource,
1327
+ linearConnected,
1328
+ issues: listed.issues,
1329
+ viewer: {
1330
+ login: listed.viewer.name,
1331
+ name: listed.viewer.name
1332
+ }
1333
+ };
1259
1334
  }
1260
- const issues = await listGitHubIssues(repoPath);
1261
- return { source, preferredSource, linearConnected, issues };
1335
+ const [issues, login] = await Promise.all([
1336
+ listGitHubIssues(repoPath),
1337
+ githubViewerLogin(repoPath)
1338
+ ]);
1339
+ return {
1340
+ source,
1341
+ preferredSource,
1342
+ linearConnected,
1343
+ issues,
1344
+ viewer: login ? { login } : void 0
1345
+ };
1262
1346
  }
1263
1347
 
1264
1348
  // src/composer/diff-comment.ts
@@ -1346,6 +1430,12 @@ function buildPastedTextAttachment(text4, opts) {
1346
1430
  }
1347
1431
 
1348
1432
  // src/plan/ask-user.ts
1433
+ function planQuestionsSignature(questions) {
1434
+ return questions.map((q) => {
1435
+ const opts = q.options.map((o) => o.label).join("");
1436
+ return `${q.question}${opts}`;
1437
+ }).join("");
1438
+ }
1349
1439
  function asRecord(v) {
1350
1440
  return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
1351
1441
  }
@@ -1403,12 +1493,22 @@ function extractPendingPlanQuestions(parts) {
1403
1493
  if (!questions.length) continue;
1404
1494
  return {
1405
1495
  id: p.id || `ask-${i}`,
1496
+ signature: planQuestionsSignature(questions),
1406
1497
  questions,
1407
1498
  source: p.name || "ask_user"
1408
1499
  };
1409
1500
  }
1410
1501
  return null;
1411
1502
  }
1503
+ function latestPendingPlanQuestions(input) {
1504
+ if (input.userReplied) return null;
1505
+ const fromLive = extractPendingPlanQuestions(input.liveParts);
1506
+ if (fromLive) return fromLive;
1507
+ const last = input.messages[input.messages.length - 1];
1508
+ if (last?.role === "user") return null;
1509
+ if (last?.role === "agent") return extractPendingPlanQuestions(last.parts);
1510
+ return null;
1511
+ }
1412
1512
  function formatPlanQuestionAnswers(questions, answers) {
1413
1513
  const lines = ["Answers to your questions:"];
1414
1514
  for (let i = 0; i < questions.length; i++) {
@@ -1447,6 +1547,7 @@ function formatPlanQuestionsForChat(questions) {
1447
1547
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1448
1548
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1449
1549
  import { z as z4 } from "zod";
1550
+ import { randomUUID as randomUUID3 } from "crypto";
1450
1551
  import { basename } from "path";
1451
1552
 
1452
1553
  // src/mcp/archive-guard.ts
@@ -1941,7 +2042,7 @@ function registerSlackTools(server) {
1941
2042
  },
1942
2043
  async ({ team_id }) => {
1943
2044
  try {
1944
- await refreshSlackReplyBadges({ force: true });
2045
+ await pollSlackOutboundWatches({ force: true });
1945
2046
  const team = team_id?.trim();
1946
2047
  const watches = listSlackOutboundWatches().filter(
1947
2048
  (w) => !team || w.teamId === team
@@ -2229,6 +2330,283 @@ function registerScheduleTools(server) {
2229
2330
  );
2230
2331
  }
2231
2332
 
2333
+ // src/board/load-home-board.ts
2334
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2335
+ import { join as join2 } from "path";
2336
+
2337
+ // src/board/board-pins.ts
2338
+ import { randomUUID as randomUUID2 } from "crypto";
2339
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2340
+ import { join } from "path";
2341
+ var FILE = "home-board-pins.json";
2342
+ var VERSION = 1;
2343
+ function pinsFile() {
2344
+ return join(appDataDir(), FILE);
2345
+ }
2346
+ function readDisk() {
2347
+ const path2 = pinsFile();
2348
+ if (!existsSync(path2)) return [];
2349
+ try {
2350
+ const raw = JSON.parse(readFileSync(path2, "utf8"));
2351
+ if (raw?.version !== VERSION || !Array.isArray(raw.items)) return [];
2352
+ return raw.items.filter((item) => item?.id && item.kind && item.ref);
2353
+ } catch {
2354
+ return [];
2355
+ }
2356
+ }
2357
+ function writeDisk(items) {
2358
+ mkdirSync(appDataDir(), { recursive: true });
2359
+ writeFileSync(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
2360
+ }
2361
+ function listBoardPins() {
2362
+ return readDisk();
2363
+ }
2364
+ function addBoardPin(input) {
2365
+ const ref = input.ref.trim();
2366
+ if (!ref) throw new Error("Board item needs a ref");
2367
+ const pin = {
2368
+ id: randomUUID2(),
2369
+ kind: input.kind,
2370
+ ref,
2371
+ repoPath: input.repoPath.trim(),
2372
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
2373
+ title: input.title?.trim() || ref,
2374
+ url: input.url,
2375
+ labels: input.labels,
2376
+ provider: input.provider,
2377
+ assignee: input.assignee,
2378
+ cycle: input.cycle,
2379
+ teamKey: input.teamKey,
2380
+ headRefName: input.headRefName,
2381
+ author: input.author,
2382
+ remoteState: "open",
2383
+ needsWorkspacePick: issueNeedsWorkspacePick(
2384
+ input.provider,
2385
+ input.workspaceCount ?? 1
2386
+ )
2387
+ };
2388
+ const items = listBoardPins();
2389
+ const identity = boardPinIdentity(pin);
2390
+ const existing = items.find((item) => boardPinIdentity(item) === identity);
2391
+ if (existing) {
2392
+ const merged = { ...existing, ...pin, id: existing.id, addedAt: existing.addedAt };
2393
+ writeDisk(items.map((item) => item.id === existing.id ? merged : item));
2394
+ return merged;
2395
+ }
2396
+ writeDisk([...items, pin]);
2397
+ return pin;
2398
+ }
2399
+ function removeBoardPin(id) {
2400
+ const items = listBoardPins();
2401
+ const next = items.filter((item) => item.id !== id);
2402
+ if (next.length === items.length) return false;
2403
+ writeDisk(next);
2404
+ return true;
2405
+ }
2406
+ function replaceBoardPins(items) {
2407
+ writeDisk(items);
2408
+ }
2409
+ function clearBoardPins() {
2410
+ try {
2411
+ unlinkSync(pinsFile());
2412
+ } catch {
2413
+ }
2414
+ }
2415
+
2416
+ // src/board/load-home-board.ts
2417
+ var CACHE_VERSION = 1;
2418
+ var memory = null;
2419
+ var inflight = null;
2420
+ function errText(err) {
2421
+ return err instanceof Error ? err.message : String(err);
2422
+ }
2423
+ function homeBoardWorkspaceKey(workspaces) {
2424
+ return workspaces.map((w) => w.path).filter(Boolean).sort().join("\n");
2425
+ }
2426
+ function cacheFile() {
2427
+ return join2(appDataDir(), "home-board-cache.json");
2428
+ }
2429
+ function emptyInputs() {
2430
+ return {
2431
+ issues: [],
2432
+ prs: [],
2433
+ issueSource: "github",
2434
+ viewerLogin: void 0,
2435
+ issueErrors: [],
2436
+ prErrors: []
2437
+ };
2438
+ }
2439
+ function asLoaded(entry, fromCache) {
2440
+ const pins = syncBoardPins(listBoardPins(), entry.inputs.issues, entry.inputs.prs);
2441
+ return { ...entry.inputs, fetchedAt: entry.fetchedAt, fromCache, pins };
2442
+ }
2443
+ function cacheStillFresh(entry, key, now) {
2444
+ return entry.workspaceKey === key && now - entry.fetchedAt < HOME_BOARD_CACHE_TTL_MS;
2445
+ }
2446
+ function shouldCacheHomeBoardInputs(inputs) {
2447
+ const issuesFailed = inputs.issues.length === 0 && inputs.issueErrors.length > 0;
2448
+ const prsFailed = inputs.prs.length === 0 && inputs.prErrors.length > 0;
2449
+ return !(issuesFailed && prsFailed);
2450
+ }
2451
+ function readDiskCache() {
2452
+ const path2 = cacheFile();
2453
+ if (!existsSync2(path2)) return null;
2454
+ try {
2455
+ const raw = JSON.parse(readFileSync2(path2, "utf8"));
2456
+ if (raw?.version !== CACHE_VERSION || typeof raw.fetchedAt !== "number") {
2457
+ return null;
2458
+ }
2459
+ if (!raw.inputs || !Array.isArray(raw.inputs.issues) || !Array.isArray(raw.inputs.prs)) {
2460
+ return null;
2461
+ }
2462
+ return raw;
2463
+ } catch {
2464
+ return null;
2465
+ }
2466
+ }
2467
+ function writeDiskCache(entry) {
2468
+ try {
2469
+ mkdirSync2(appDataDir(), { recursive: true });
2470
+ writeFileSync2(cacheFile(), JSON.stringify(entry), "utf8");
2471
+ } catch {
2472
+ }
2473
+ }
2474
+ function deleteDiskCache() {
2475
+ try {
2476
+ unlinkSync2(cacheFile());
2477
+ } catch {
2478
+ }
2479
+ }
2480
+ function resetHomeBoardMemory() {
2481
+ memory = null;
2482
+ inflight = null;
2483
+ }
2484
+ function clearHomeBoardCache() {
2485
+ resetHomeBoardMemory();
2486
+ deleteDiskCache();
2487
+ }
2488
+ async function loadHomeBoardInputs(workspaces) {
2489
+ const paths = workspaces.map((w) => w.path).filter(Boolean);
2490
+ if (paths.length === 0) return emptyInputs();
2491
+ const issueErrors = [];
2492
+ const prErrors = [];
2493
+ let issueSource = "github";
2494
+ let viewerLogin;
2495
+ let issues = [];
2496
+ try {
2497
+ const first = await listIssues(paths[0]);
2498
+ issueSource = first.source;
2499
+ viewerLogin = first.viewer?.login || first.viewer?.name || void 0;
2500
+ if (first.source === "linear") {
2501
+ const repoPath = paths[0] ?? "";
2502
+ issues = first.issues.map((issue) => ({
2503
+ ...issue,
2504
+ repoPath,
2505
+ needsWorkspacePick: issueNeedsWorkspacePick(
2506
+ issue.provider ?? first.source,
2507
+ paths.length
2508
+ )
2509
+ }));
2510
+ } else {
2511
+ const settled = await Promise.allSettled(
2512
+ paths.map(async (path2) => {
2513
+ const result = path2 === paths[0] ? first : await listIssues(path2);
2514
+ return result.issues.map((issue) => ({
2515
+ ...issue,
2516
+ repoPath: path2,
2517
+ needsWorkspacePick: issueNeedsWorkspacePick(
2518
+ issue.provider ?? result.source,
2519
+ 1
2520
+ )
2521
+ }));
2522
+ })
2523
+ );
2524
+ const collected = [];
2525
+ for (const item of settled) {
2526
+ if (item.status === "fulfilled") collected.push(...item.value);
2527
+ else issueErrors.push(errText(item.reason));
2528
+ }
2529
+ issues = collected;
2530
+ if (collected.length === 0 && issueErrors[0]) {
2531
+ throw new Error(issueErrors[0]);
2532
+ }
2533
+ }
2534
+ issues = dedupeBoardIssues(issues);
2535
+ } catch (err) {
2536
+ issueErrors.push(errText(err));
2537
+ issues = [];
2538
+ }
2539
+ let prs = [];
2540
+ try {
2541
+ const settled = await Promise.allSettled(
2542
+ paths.map(async (path2) => {
2543
+ const list = await listPrs(path2);
2544
+ return list.map((pr) => ({ ...pr, repoPath: path2 }));
2545
+ })
2546
+ );
2547
+ const collected = [];
2548
+ for (const item of settled) {
2549
+ if (item.status === "fulfilled") collected.push(...item.value);
2550
+ else prErrors.push(errText(item.reason));
2551
+ }
2552
+ if (collected.length === 0 && prErrors[0]) {
2553
+ throw new Error(prErrors[0]);
2554
+ }
2555
+ prs = dedupeBoardPrs(collected);
2556
+ } catch (err) {
2557
+ prErrors.push(errText(err));
2558
+ prs = [];
2559
+ }
2560
+ return { issues, prs, issueSource, viewerLogin, issueErrors, prErrors };
2561
+ }
2562
+ async function getHomeBoardInputs(workspaces, opts) {
2563
+ const key = homeBoardWorkspaceKey(workspaces);
2564
+ const now = opts?.now ?? Date.now();
2565
+ if (!key) {
2566
+ return {
2567
+ ...emptyInputs(),
2568
+ fetchedAt: now,
2569
+ fromCache: false,
2570
+ pins: listBoardPins()
2571
+ };
2572
+ }
2573
+ if (!opts?.refresh) {
2574
+ const disk = readDiskCache();
2575
+ const best = memory && disk ? disk.fetchedAt >= memory.fetchedAt ? disk : memory : memory ?? disk;
2576
+ if (best && cacheStillFresh(best, key, now)) {
2577
+ memory = best;
2578
+ return asLoaded(best, true);
2579
+ }
2580
+ }
2581
+ if (inflight && inflight.key === key) {
2582
+ return inflight.promise;
2583
+ }
2584
+ const promise = (async () => {
2585
+ const inputs = await loadHomeBoardInputs(workspaces);
2586
+ const fetchedAt = opts?.now ?? Date.now();
2587
+ const pins = syncBoardPins(listBoardPins(), inputs.issues, inputs.prs);
2588
+ replaceBoardPins(pins);
2589
+ const loaded = { ...inputs, fetchedAt, fromCache: false, pins };
2590
+ if (shouldCacheHomeBoardInputs(inputs)) {
2591
+ const entry = {
2592
+ version: CACHE_VERSION,
2593
+ workspaceKey: key,
2594
+ fetchedAt,
2595
+ inputs
2596
+ };
2597
+ memory = entry;
2598
+ writeDiskCache(entry);
2599
+ }
2600
+ return loaded;
2601
+ })();
2602
+ inflight = { key, promise };
2603
+ try {
2604
+ return await promise;
2605
+ } finally {
2606
+ if (inflight?.promise === promise) inflight = null;
2607
+ }
2608
+ }
2609
+
2232
2610
  // src/mcp/server.ts
2233
2611
  var MAX_ORCH_THREADS = 5;
2234
2612
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -2249,6 +2627,93 @@ function withTimeout(promise, ms, label) {
2249
2627
  );
2250
2628
  });
2251
2629
  }
2630
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
2631
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
2632
+ let parentId = args.parentThreadId?.trim() || "";
2633
+ let parent = parentId ? orch.getThread(parentId) : null;
2634
+ let parentCorrectedFrom;
2635
+ if (envParentId) {
2636
+ const envParent = orch.getThread(envParentId);
2637
+ if (envParent) {
2638
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
2639
+ else if (!parentId) parentCorrectedFrom = void 0;
2640
+ parentId = envParentId;
2641
+ parent = envParent;
2642
+ }
2643
+ }
2644
+ if (parentId && !parent) {
2645
+ parentCorrectedFrom = parentId;
2646
+ parentId = "";
2647
+ parent = null;
2648
+ }
2649
+ if (parentId) {
2650
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
2651
+ if (children.length >= MAX_ORCH_THREADS) {
2652
+ return {
2653
+ ok: false,
2654
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
2655
+ };
2656
+ }
2657
+ }
2658
+ let agentArg = args.agent;
2659
+ let agentCoercedFrom;
2660
+ const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
2661
+ if (resolvedProbe === "codex") {
2662
+ agentCoercedFrom = agentArg ?? "codex";
2663
+ const accountAgent = resolveNewThreadOptions2({}).agent;
2664
+ agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
2665
+ }
2666
+ const opts = resolveNewThreadOptions2({
2667
+ agent: agentArg,
2668
+ model: args.model
2669
+ });
2670
+ try {
2671
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
2672
+ const thread = await withTimeout(
2673
+ orch.createThread({
2674
+ sourceType: args.sourceType,
2675
+ sourceRef: args.sourceRef,
2676
+ agent: opts.agent,
2677
+ model: opts.model,
2678
+ effort: opts.effort,
2679
+ fast: opts.fast,
2680
+ repoPath: args.repoPath,
2681
+ title: args.title,
2682
+ parentThreadId: parentId || null,
2683
+ cowboy: args.cowboy || void 0,
2684
+ attachments: args.attachments
2685
+ }),
2686
+ CREATE_THREAD_TIMEOUT_MS,
2687
+ "create_thread"
2688
+ );
2689
+ const alreadyStarted = priorIds.has(thread.id);
2690
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
2691
+ return {
2692
+ ok: true,
2693
+ text: JSON.stringify({
2694
+ id: thread.id,
2695
+ title: thread.title,
2696
+ branchName: thread.branchName,
2697
+ worktreePath: thread.worktreePath,
2698
+ agent: thread.agent,
2699
+ model: thread.model,
2700
+ status: thread.status,
2701
+ cowboy: Boolean(thread.cowboy),
2702
+ link: `sideboard://thread/${thread.id}`,
2703
+ parentThreadId: thread.parentThreadId,
2704
+ ...alreadyStarted ? { alreadyStarted: true } : {},
2705
+ ...agentCoercedFrom ? {
2706
+ agentCoercedFrom,
2707
+ note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
2708
+ } : {},
2709
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
2710
+ })
2711
+ };
2712
+ } catch (err) {
2713
+ const message = err instanceof Error ? err.message : String(err);
2714
+ return { ok: false, text: `create_thread failed: ${message}` };
2715
+ }
2716
+ }
2252
2717
  async function startMcpServer() {
2253
2718
  const orch = getOrchestrator();
2254
2719
  try {
@@ -2260,7 +2725,7 @@ async function startMcpServer() {
2260
2725
  );
2261
2726
  }
2262
2727
  try {
2263
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-YFWV7MOG.js");
2728
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-6RPALX4J.js");
2264
2729
  orch.setMaxConcurrent(maxConcurrentAgents2());
2265
2730
  } catch {
2266
2731
  }
@@ -2280,7 +2745,7 @@ async function startMcpServer() {
2280
2745
  if (!worktreeProfile) {
2281
2746
  server.tool(
2282
2747
  "list_workspaces",
2283
- "List registered Sideboard workspaces (repos). Each line is name, path, and github:owner/repo when resolvable \u2014 use path as repoPath for list_branches/list_prs/list_issues/create_thread.",
2748
+ "List registered Sideboard workspaces (repos). Each line is name, path, and github:owner/repo when resolvable \u2014 use path as repoPath for list_board/list_branches/list_prs/list_issues/create_thread.",
2284
2749
  {},
2285
2750
  async () => {
2286
2751
  const workspaces = orch.listWorkspaces();
@@ -2314,6 +2779,51 @@ async function startMcpServer() {
2314
2779
  };
2315
2780
  }
2316
2781
  );
2782
+ server.tool(
2783
+ "list_board",
2784
+ "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), column, limit (default 40). create_thread adds a worktree (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
2785
+ {
2786
+ query: z4.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
2787
+ repoPath: z4.string().optional().describe("Limit to one workspace path from list_workspaces"),
2788
+ kind: z4.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
2789
+ column: z4.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
2790
+ "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
2791
+ ),
2792
+ limit: z4.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
2793
+ },
2794
+ async ({ query, repoPath, kind, column, limit }) => {
2795
+ const workspaces = orch.listWorkspaces();
2796
+ const all = orch.getThreads(true);
2797
+ const names = new Map(workspaces.map((w) => [w.path, w.name]));
2798
+ const snap = assembleHomeBoard({
2799
+ threads: all.filter((t) => t.status !== "archived"),
2800
+ query,
2801
+ repoPath,
2802
+ kind: kind ?? "all",
2803
+ column: column === "needs_you" ? "new" : column,
2804
+ limit,
2805
+ workspaceName: (path2) => names.get(path2) ?? ""
2806
+ });
2807
+ return {
2808
+ content: [
2809
+ {
2810
+ type: "text",
2811
+ text: JSON.stringify(
2812
+ {
2813
+ columns: snap.columns,
2814
+ hidden: snap.hidden,
2815
+ totals: snap.totals,
2816
+ columnDefs: BOARD_COLUMN_DEFS,
2817
+ hint: HOME_BOARD_AGENT_HINT
2818
+ },
2819
+ null,
2820
+ 2
2821
+ )
2822
+ }
2823
+ ]
2824
+ };
2825
+ }
2826
+ );
2317
2827
  server.tool(
2318
2828
  "get_thread",
2319
2829
  "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
@@ -2483,7 +2993,7 @@ async function startMcpServer() {
2483
2993
  registerLinearTools(server);
2484
2994
  registerScheduleTools(server);
2485
2995
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-X3JNPR42.js");
2486
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-YFWV7MOG.js");
2996
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-6RPALX4J.js");
2487
2997
  const accountDefaults = resolveThreadDefaults2();
2488
2998
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
2489
2999
  server.tool(
@@ -2526,7 +3036,7 @@ async function startMcpServer() {
2526
3036
  );
2527
3037
  server.tool(
2528
3038
  "create_thread",
2529
- `Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
3039
+ `Create a worktree thread (chat) from branch, pr, or ticket. If a live worktree already matches that ticket, PR, or named branch, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
2530
3040
  {
2531
3041
  sourceType: z4.enum(["branch", "pr", "ticket"]),
2532
3042
  sourceRef: z4.string(),
@@ -2544,104 +3054,118 @@ async function startMcpServer() {
2544
3054
  )
2545
3055
  },
2546
3056
  async (args) => {
2547
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
2548
- let parentId = args.parentThreadId?.trim() || "";
2549
- let parent = parentId ? orch.getThread(parentId) : null;
2550
- let parentCorrectedFrom;
2551
- if (envParentId) {
2552
- const envParent = orch.getThread(envParentId);
2553
- if (envParent) {
2554
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
2555
- else if (!parentId) parentCorrectedFrom = void 0;
2556
- parentId = envParentId;
2557
- parent = envParent;
2558
- }
2559
- }
2560
- if (parentId && !parent) {
2561
- parentCorrectedFrom = parentId;
2562
- parentId = "";
2563
- parent = null;
2564
- }
2565
- if (parentId) {
2566
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
2567
- if (children.length >= MAX_ORCH_THREADS) {
2568
- return {
2569
- content: [
2570
- {
2571
- type: "text",
2572
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
2573
- }
2574
- ],
2575
- isError: true
2576
- };
2577
- }
2578
- }
2579
- let agentArg = args.agent;
2580
- let agentCoercedFrom;
2581
- const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
2582
- if (resolvedProbe === "codex") {
2583
- agentCoercedFrom = agentArg ?? "codex";
2584
- const accountAgent = resolveNewThreadOptions2({}).agent;
2585
- agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
2586
- }
2587
- const opts = resolveNewThreadOptions2({
2588
- agent: agentArg,
2589
- model: args.model
2590
- });
2591
- try {
2592
- const thread = await withTimeout(
2593
- orch.createThread({
2594
- sourceType: args.sourceType,
2595
- sourceRef: args.sourceRef,
2596
- agent: opts.agent,
2597
- model: opts.model,
2598
- effort: opts.effort,
2599
- fast: opts.fast,
2600
- repoPath: args.repoPath,
2601
- title: args.title,
2602
- parentThreadId: parentId || null,
2603
- cowboy: args.cowboy || void 0
2604
- }),
2605
- CREATE_THREAD_TIMEOUT_MS,
2606
- "create_thread"
2607
- );
2608
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
3057
+ const result = await createOrchChildThread(orch, args, resolveNewThreadOptions2);
3058
+ return {
3059
+ content: [{ type: "text", text: result.text }],
3060
+ ...result.ok ? {} : { isError: true }
3061
+ };
3062
+ }
3063
+ );
3064
+ server.tool(
3065
+ "start_board_card",
3066
+ "Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Reuses the live worktree if one already matches. Then send_to_thread.",
3067
+ {
3068
+ kind: z4.enum(["ticket", "pr", "branch"]),
3069
+ ref: z4.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
3070
+ repoPath: z4.string().describe("Workspace path from list_workspaces / list_board"),
3071
+ title: z4.string().optional()
3072
+ },
3073
+ async ({ kind, ref, repoPath, title }) => {
3074
+ const root = await resolveRepoRoot(repoPath);
3075
+ const existing = findLiveThreadForCreate(
3076
+ {
3077
+ sourceType: kind,
3078
+ sourceRef: ref.trim(),
3079
+ repoPath: canonicalizeRepoPath(root),
3080
+ title: title?.trim()
3081
+ },
3082
+ orch.getThreads(false).map((t) => ({
3083
+ ...t,
3084
+ repoPath: canonicalizeRepoPath(t.repoPath)
3085
+ }))
3086
+ );
3087
+ if (existing) {
2609
3088
  return {
2610
3089
  content: [
2611
3090
  {
2612
3091
  type: "text",
2613
3092
  text: JSON.stringify({
2614
- id: thread.id,
2615
- title: thread.title,
2616
- branchName: thread.branchName,
2617
- worktreePath: thread.worktreePath,
2618
- agent: thread.agent,
2619
- model: thread.model,
2620
- status: thread.status,
2621
- cowboy: Boolean(thread.cowboy),
2622
- link: `sideboard://thread/${thread.id}`,
2623
- parentThreadId: thread.parentThreadId,
2624
- ...agentCoercedFrom ? {
2625
- agentCoercedFrom,
2626
- note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
2627
- } : {},
2628
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
3093
+ alreadyStarted: true,
3094
+ id: existing.id,
3095
+ title: existing.title,
3096
+ status: existing.status,
3097
+ link: `sideboard://thread/${existing.id}`
2629
3098
  })
2630
3099
  }
2631
3100
  ]
2632
3101
  };
2633
- } catch (err) {
2634
- const message = err instanceof Error ? err.message : String(err);
3102
+ }
3103
+ const workspaces = orch.listWorkspaces();
3104
+ const loaded = await getHomeBoardInputs(workspaces);
3105
+ const pin = findBoardPin(loaded.pins, kind, ref, root);
3106
+ if (kind === "branch") {
3107
+ const sourceRef = pin?.ref === "default" ? "default" : pin?.ref ?? ref.trim();
3108
+ const result2 = await createOrchChildThread(
3109
+ orch,
3110
+ {
3111
+ sourceType: "branch",
3112
+ sourceRef,
3113
+ repoPath: root,
3114
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
3115
+ },
3116
+ resolveNewThreadOptions2
3117
+ );
2635
3118
  return {
2636
- content: [
2637
- {
2638
- type: "text",
2639
- text: `create_thread failed: ${message}`
2640
- }
2641
- ],
2642
- isError: true
3119
+ content: [{ type: "text", text: result2.text }],
3120
+ ...result2.ok ? {} : { isError: true }
2643
3121
  };
2644
3122
  }
3123
+ if (kind === "ticket") {
3124
+ const issue = findBoardIssue(loaded.issues, ref, root);
3125
+ const ident = issue?.identifier ?? ref.trim();
3126
+ const attachments = issue ? [
3127
+ {
3128
+ id: randomUUID3(),
3129
+ name: issue.identifier,
3130
+ kind: "issue",
3131
+ content: [
3132
+ `Linked issue: ${issue.identifier} \u2014 ${issue.title}`,
3133
+ issue.url ? `URL: ${issue.url}` : null
3134
+ ].filter(Boolean).join("\n")
3135
+ }
3136
+ ] : void 0;
3137
+ const result2 = await createOrchChildThread(
3138
+ orch,
3139
+ {
3140
+ sourceType: "ticket",
3141
+ sourceRef: ident,
3142
+ repoPath: root,
3143
+ title: title?.trim() || issue?.title,
3144
+ attachments
3145
+ },
3146
+ resolveNewThreadOptions2
3147
+ );
3148
+ return {
3149
+ content: [{ type: "text", text: result2.text }],
3150
+ ...result2.ok ? {} : { isError: true }
3151
+ };
3152
+ }
3153
+ const pr = findBoardPr(loaded.prs, ref, root);
3154
+ const number = pr ? String(pr.number) : ref.trim().replace(/^#/, "");
3155
+ const result = await createOrchChildThread(
3156
+ orch,
3157
+ {
3158
+ sourceType: "pr",
3159
+ sourceRef: number,
3160
+ repoPath: root,
3161
+ title: title?.trim() || pr?.title
3162
+ },
3163
+ resolveNewThreadOptions2
3164
+ );
3165
+ return {
3166
+ content: [{ type: "text", text: result.text }],
3167
+ ...result.ok ? {} : { isError: true }
3168
+ };
2645
3169
  }
2646
3170
  );
2647
3171
  server.tool(
@@ -3288,7 +3812,7 @@ async function startMcpServer() {
3288
3812
  );
3289
3813
  server.tool(
3290
3814
  "list_issues",
3291
- "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
3815
+ "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Linear returns issues assigned to you (includes cycle). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Prefer list_board for Home columns + current-cycle filter. Then create_thread with sourceType=ticket.",
3292
3816
  { repoPath: z4.string() },
3293
3817
  async ({ repoPath }) => {
3294
3818
  const root = await resolveRepoRoot(repoPath);
@@ -3630,14 +4154,14 @@ async function runCloudConnect(opts) {
3630
4154
  }
3631
4155
 
3632
4156
  // src/agents/user-mcp-config.ts
3633
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4157
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3634
4158
  import { homedir } from "os";
3635
- import { dirname, join } from "path";
4159
+ import { dirname, join as join3 } from "path";
3636
4160
  function userCursorMcpConfigPath() {
3637
- return join(homedir(), ".cursor", "mcp.json");
4161
+ return join3(homedir(), ".cursor", "mcp.json");
3638
4162
  }
3639
4163
  function userClaudeMcpConfigPath() {
3640
- return join(homedir(), ".claude.json");
4164
+ return join3(homedir(), ".claude.json");
3641
4165
  }
3642
4166
  function asObject(value) {
3643
4167
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
@@ -3664,16 +4188,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
3664
4188
  }
3665
4189
  function writeMergedMcpServersJson(configPath, sideboard) {
3666
4190
  let existing = {};
3667
- if (existsSync(configPath)) {
4191
+ if (existsSync3(configPath)) {
3668
4192
  try {
3669
- existing = JSON.parse(readFileSync(configPath, "utf8"));
4193
+ existing = JSON.parse(readFileSync3(configPath, "utf8"));
3670
4194
  } catch {
3671
4195
  existing = {};
3672
4196
  }
3673
4197
  }
3674
4198
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
3675
- mkdirSync(dirname(configPath), { recursive: true });
3676
- writeFileSync(configPath, `${JSON.stringify(next, null, 2)}
4199
+ mkdirSync3(dirname(configPath), { recursive: true });
4200
+ writeFileSync3(configPath, `${JSON.stringify(next, null, 2)}
3677
4201
  `);
3678
4202
  }
3679
4203
  function launchFromResolved(server) {
@@ -3691,7 +4215,7 @@ async function registerPackagedUserMcpClients() {
3691
4215
  const cursor = userCursorMcpConfigPath();
3692
4216
  writeMergedMcpServersJson(cursor, launch);
3693
4217
  const claude = userClaudeMcpConfigPath();
3694
- if (existsSync(claude)) {
4218
+ if (existsSync3(claude)) {
3695
4219
  writeMergedMcpServersJson(claude, launch);
3696
4220
  return { cursor, claude };
3697
4221
  }
@@ -5620,6 +6144,8 @@ export {
5620
6144
  GITHUB_GIT_AUTH_MODES,
5621
6145
  GLOBAL_WORKSPACE_ID,
5622
6146
  HARNESS_ENV_KEYS,
6147
+ HOME_BOARD_CACHE_TTL_MS,
6148
+ ISSUE_SOURCE_LABELS,
5623
6149
  LEGACY_ATTACHMENTS_DIR,
5624
6150
  LEGACY_PLAN_FILE_REL,
5625
6151
  LEGACY_REVIEW_REQUEST_PATH,
@@ -5661,6 +6187,7 @@ export {
5661
6187
  THINKING_EFFORTS,
5662
6188
  WORKTREE_MCP_TOOLS,
5663
6189
  ackSlackInboundSeen,
6190
+ addBoardPin,
5664
6191
  addPrStackLayer,
5665
6192
  addStackLayerFromThread,
5666
6193
  addWorkspace,
@@ -5714,15 +6241,19 @@ export {
5714
6241
  caffeinateWhileRunningEnabled,
5715
6242
  caffeinateWhileSchedulesEnabled,
5716
6243
  caffeinateWhileSlackListenEnabled,
6244
+ canonicalizeRepoPath,
5717
6245
  captureLoginEnv,
5718
6246
  captureTurnBaseline,
5719
6247
  checkoutPrStackLayer,
5720
6248
  childEnvWithAppSettings,
5721
6249
  claimDesktopHost,
6250
+ classifyWorktreeColumn,
5722
6251
  claudeAdapter,
5723
6252
  claudeChromeEnabled,
5724
6253
  claudeUserSettingsPath,
5725
6254
  cleanupOrphanWorktrees,
6255
+ clearBoardPins,
6256
+ clearHomeBoardCache,
5726
6257
  cloneRepoIntoSideboard,
5727
6258
  codexAdapter,
5728
6259
  codexSandboxWritableRootsArgs,
@@ -5771,7 +6302,6 @@ export {
5771
6302
  disconnectLinearConnection,
5772
6303
  disconnectSlackWorkspace,
5773
6304
  discoverSkills,
5774
- dismissSlackReplyBadge,
5775
6305
  dropCachedPrefixOnResume,
5776
6306
  encodeBrightsyTarget,
5777
6307
  enrichPathWithNpmGlobalBin,
@@ -5796,6 +6326,7 @@ export {
5796
6326
  finalizeParts,
5797
6327
  findConventionSetup,
5798
6328
  findInvalidCacheControlTtlOrder,
6329
+ findLiveThreadForCreate,
5799
6330
  findOrphanWorktrees,
5800
6331
  findSlackCoordinator,
5801
6332
  findThreadByRef,
@@ -5847,6 +6378,7 @@ export {
5847
6378
  getGitHubStatus,
5848
6379
  getGithubGitAuthMode,
5849
6380
  getGithubPat,
6381
+ getHomeBoardInputs,
5850
6382
  getIssueSource,
5851
6383
  getLinearApiKey,
5852
6384
  getLinearAuthToken,
@@ -5869,6 +6401,7 @@ export {
5869
6401
  git,
5870
6402
  githubAgentGitEnv,
5871
6403
  globalAgentCwd,
6404
+ groupHomeBoardWorktrees,
5872
6405
  handleSlackInbound,
5873
6406
  harnessEnvKey,
5874
6407
  hasBakedLinearOAuth,
@@ -5902,9 +6435,11 @@ export {
5902
6435
  isGhRateLimitError,
5903
6436
  isGlobalRepoPath,
5904
6437
  isGlobalThread,
6438
+ isHomeBoardThread,
5905
6439
  isImageFilePath,
5906
6440
  isInPrStack,
5907
6441
  isInboundForThisDesktop,
6442
+ isIssueSourceConnected,
5908
6443
  isLinearConnected,
5909
6444
  isLinearOAuthCancelled,
5910
6445
  isOrchestratorCapableAgent,
@@ -5927,12 +6462,16 @@ export {
5927
6462
  isThreadCaffeinated,
5928
6463
  isThreadRecordFile,
5929
6464
  isWorkspaceScratchPath,
6465
+ issueSourceLabel,
5930
6466
  lastRequestOccupancy,
6467
+ latestPendingPlanQuestions,
5931
6468
  linearAuthorizationHeader,
6469
+ linearCycleIsActive,
5932
6470
  linearGraphql,
5933
6471
  linearOAuthAuthorizeUrl,
5934
6472
  linearOAuthCredentials,
5935
6473
  listAgentSetupInfo,
6474
+ listBoardPins,
5936
6475
  listBranchCommits,
5937
6476
  listBranches,
5938
6477
  listBrightsyAccounts,
@@ -5944,6 +6483,7 @@ export {
5944
6483
  listGitHubIssues,
5945
6484
  listGlobalThreads,
5946
6485
  listIssues,
6486
+ listLinearAssignedIssues,
5947
6487
  listLinearIssues,
5948
6488
  listLinearIssuesDirect,
5949
6489
  listLinearTeams,
@@ -5953,7 +6493,6 @@ export {
5953
6493
  listRunScripts,
5954
6494
  listSchedules,
5955
6495
  listSlackOutboundWatches,
5956
- listSlackReplyBadges,
5957
6496
  listSlackWorkspaces,
5958
6497
  listThreads,
5959
6498
  listWorkspaces,
@@ -5964,6 +6503,7 @@ export {
5964
6503
  loadAppSettings,
5965
6504
  loadBrightsyConfig,
5966
6505
  loadConductorSettings,
6506
+ loadHomeBoardInputs,
5967
6507
  loadRepoSettings,
5968
6508
  loadWorkspaceSettings,
5969
6509
  locksDir,
@@ -6009,10 +6549,11 @@ export {
6009
6549
  partsToAssistantText,
6010
6550
  pastedTextStats,
6011
6551
  pendingSlackExternalReplies,
6012
- permalinkForSlackReplyBadge,
6013
6552
  permissionMode,
6014
6553
  persistVaultKeyInKeychain,
6015
6554
  planFileAbs,
6555
+ planQuestionsSignature,
6556
+ pollSlackOutboundWatches,
6016
6557
  posixShellSingleQuote,
6017
6558
  preferredCursorCostCents,
6018
6559
  prepareTerminalCommand,
@@ -6030,10 +6571,10 @@ export {
6030
6571
  recordScheduleRun,
6031
6572
  recordSlackOutboundWatch,
6032
6573
  refreshGitHubAuth,
6033
- refreshSlackReplyBadges,
6034
6574
  registerPackagedUserMcpClients,
6035
6575
  releaseCaffeinateHoldForThread,
6036
6576
  releaseDesktopHost,
6577
+ removeBoardPin,
6037
6578
  removeWorkspace,
6038
6579
  removeWorktree,
6039
6580
  repoSlug,
@@ -6180,6 +6721,7 @@ export {
6180
6721
  withMaxOldSpaceSize,
6181
6722
  withThreadLock,
6182
6723
  workspaceSettingsSourceLabel,
6724
+ worktreeBoardStatus,
6183
6725
  worktreeCleanupSettings,
6184
6726
  worktreeDisplayLabel,
6185
6727
  worktreeDisplayLabelForGroup,