@sideboard-ai/core 0.1.125 → 0.1.130

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-GKY2GR2J.js → chunk-FMBLOOAO.js} +484 -37
  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-UAVZ2JSO.js → chunk-UDQI3N47.js} +14 -6
  17. package/dist/{chunk-HZLE6LJJ.js → chunk-VE22NWBD.js} +2 -2
  18. package/dist/{chunk-ED27HCPV.js → chunk-YALVX3UE.js} +496 -37
  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 +1233 -170
  26. package/dist/index.d.cts +233 -10
  27. package/dist/index.d.ts +233 -10
  28. package/dist/index.js +695 -131
  29. package/dist/mcp/run-stdio.cjs +1081 -149
  30. package/dist/mcp/run-stdio.js +579 -123
  31. package/dist/{orchestrator-FZYM7I42.js → orchestrator-3ZSFTG72.js} +8 -8
  32. package/dist/{orchestrator-HN3LYHFN.js → orchestrator-G6DU3AF3.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,6 +52,8 @@ import {
46
52
  createPrStack,
47
53
  createSchedule,
48
54
  createThread,
55
+ dedupeBoardIssues,
56
+ dedupeBoardPrs,
49
57
  defaultScheduleName,
50
58
  deleteSchedule,
51
59
  desktopHostPidPath,
@@ -58,6 +66,10 @@ import {
58
66
  estimateThreadChars,
59
67
  expandComposerPrompt,
60
68
  extractiveSummary,
69
+ findBoardIssue,
70
+ findBoardPin,
71
+ findBoardPr,
72
+ findLiveThreadForCreate,
61
73
  findOrphanWorktrees,
62
74
  findThreadForStackLayer,
63
75
  fireSchedule,
@@ -87,6 +99,7 @@ import {
87
99
  getSchedule,
88
100
  getSlackReplyTarget,
89
101
  getSlackWorkspace,
102
+ groupHomeBoardWorktrees,
90
103
  hasEnabledSchedules,
91
104
  importConductorWorkspace,
92
105
  importConductorWorkspaceAsync,
@@ -95,11 +108,13 @@ import {
95
108
  inspectGitWorktree,
96
109
  isCowboyThread,
97
110
  isDesktopHostAlive,
111
+ isHomeBoardThread,
98
112
  isImageFilePath,
99
113
  isPidAlive,
100
114
  isPrimaryCheckoutThread,
101
115
  isSlackExternalReplyPrompt,
102
116
  isThisProcessDesktopHost,
117
+ issueNeedsWorkspacePick,
103
118
  lastRequestOccupancy,
104
119
  listBranchCommits,
105
120
  listConductorWorkspaces,
@@ -158,6 +173,7 @@ import {
158
173
  startDevServer,
159
174
  startOrchestration,
160
175
  summarizeConversation,
176
+ syncBoardPins,
161
177
  takenTeamSlugsForChatTab,
162
178
  thisProcessShouldDrainAgentQueues,
163
179
  threadsSharingWorktree,
@@ -165,10 +181,11 @@ import {
165
181
  upsertSlackWorkspace,
166
182
  waitForPidExit,
167
183
  withAgentInstructions,
184
+ worktreeBoardStatus,
168
185
  worktreeCleanupSettings,
169
186
  wrapReviewSkillMarkdown,
170
187
  writeWorktreeFile
171
- } from "./chunk-ED27HCPV.js";
188
+ } from "./chunk-YALVX3UE.js";
172
189
  import {
173
190
  BRIGHTSY_MCP_ALLOWED_TOOLS,
174
191
  CLAUDE_MODEL_CATALOG,
@@ -247,7 +264,7 @@ import {
247
264
  withEventParentId,
248
265
  withEventsParentId,
249
266
  writeInjectedMcpConfig
250
- } from "./chunk-7Y3AYQWT.js";
267
+ } from "./chunk-NV73AO7Q.js";
251
268
  import {
252
269
  brightsyConfigPath,
253
270
  brightsyMcpServerName,
@@ -266,7 +283,7 @@ import {
266
283
  listWorkspaces,
267
284
  removeWorkspace,
268
285
  syncWorkspacesFromThreads
269
- } from "./chunk-HZLE6LJJ.js";
286
+ } from "./chunk-VE22NWBD.js";
270
287
  import {
271
288
  CLOUD_COORDINATOR_BUSY_REPLY,
272
289
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -294,7 +311,7 @@ import {
294
311
  parseForceStopMessage,
295
312
  slackCoordinatorSourceRef,
296
313
  takenTeamSlugsForOrchestration
297
- } from "./chunk-KWGP6RZM.js";
314
+ } from "./chunk-HULESWLI.js";
298
315
  import {
299
316
  COORDINATOR_TOOL_PLAYBOOK,
300
317
  SLACK_REPLY_FORMATTING,
@@ -303,7 +320,7 @@ import {
303
320
  enrichWorkspacesWithGithub,
304
321
  ensureGlobalCoordinatorCwd,
305
322
  formatWorkspaceInventory
306
- } from "./chunk-2X7I6MDW.js";
323
+ } from "./chunk-HZPO3SSZ.js";
307
324
  import {
308
325
  LEGACY_PLAN_FILE_REL,
309
326
  PLAN_FILE_NAME,
@@ -340,6 +357,7 @@ import {
340
357
  appendIndexedGitConfig,
341
358
  applyGithubGitAuthEnv,
342
359
  branchDisplayLabel,
360
+ canonicalizeRepoPath,
343
361
  checkoutPrStackLayer,
344
362
  codexSandboxWritableRootsArgs,
345
363
  codexUnattendedGitConfigArgs,
@@ -412,7 +430,7 @@ import {
412
430
  worktreeDisplayLabel,
413
431
  worktreeDisplayLabelForGroup,
414
432
  worktreeNameFromPath
415
- } from "./chunk-UAVZ2JSO.js";
433
+ } from "./chunk-UDQI3N47.js";
416
434
  import {
417
435
  ATTACHMENTS_DIR,
418
436
  LEGACY_ATTACHMENTS_DIR,
@@ -422,6 +440,7 @@ import {
422
440
  import {
423
441
  GITHUB_GIT_AUTH_MODES,
424
442
  HARNESS_ENV_KEYS,
443
+ ISSUE_SOURCE_LABELS,
425
444
  appSettingsPath,
426
445
  applyAppEnvironment,
427
446
  autoArchiveOnMergeEnabled,
@@ -451,7 +470,9 @@ import {
451
470
  getIssueSource,
452
471
  getLinearApiKey,
453
472
  harnessEnvKey,
473
+ isIssueSourceConnected,
454
474
  isLinearConnected,
475
+ issueSourceLabel,
455
476
  loadAppSettings,
456
477
  maxConcurrentAgents,
457
478
  orchestrationQuotaFallbackAgent,
@@ -481,7 +502,7 @@ import {
481
502
  updateDefaultsSettings,
482
503
  updateIntegrationsSettings,
483
504
  updateOpencodeSettings
484
- } from "./chunk-FXQPY2KU.js";
505
+ } from "./chunk-HUYEU4GS.js";
485
506
  import {
486
507
  stripNestedElectronEnv
487
508
  } from "./chunk-4TR3HZFT.js";
@@ -500,7 +521,7 @@ import {
500
521
  updateThread,
501
522
  withThreadLock,
502
523
  writeThread
503
- } from "./chunk-HPAWHIZ3.js";
524
+ } from "./chunk-HLJUNJBF.js";
504
525
  import {
505
526
  THINKING_EFFORTS,
506
527
  isThinkingEffort,
@@ -898,6 +919,16 @@ async function disconnectLinear() {
898
919
 
899
920
  // src/integrations/linear.ts
900
921
  var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
922
+ var LIST_ISSUE_FIELDS = `
923
+ id
924
+ identifier
925
+ title
926
+ url
927
+ assignee { id name }
928
+ team { id key }
929
+ labels(first: 10) { nodes { name } }
930
+ cycle { name number startsAt endsAt completedAt }
931
+ `;
901
932
  var ISSUE_FIELDS = `
902
933
  id
903
934
  identifier
@@ -907,18 +938,21 @@ var ISSUE_FIELDS = `
907
938
  priority
908
939
  state { id name type }
909
940
  assignee { id name }
910
- team { id key name states { nodes { id name type } } }
911
- labels { nodes { name } }
941
+ team { id key name states(first: 50) { nodes { id name type } } }
942
+ labels(first: 50) { nodes { name } }
943
+ cycle { name number startsAt endsAt completedAt }
912
944
  `;
913
945
  var ASSIGNED_ISSUES_QUERY = `
914
946
  query SideboardAssignedIssues($first: Int!) {
915
947
  viewer {
948
+ id
949
+ name
916
950
  assignedIssues(
917
951
  first: $first
918
952
  orderBy: updatedAt
919
953
  filter: { state: { type: { nin: ["completed", "canceled"] } } }
920
954
  ) {
921
- nodes { ${ISSUE_FIELDS} }
955
+ nodes { ${LIST_ISSUE_FIELDS} }
922
956
  }
923
957
  }
924
958
  }
@@ -931,7 +965,7 @@ query SideboardTeams {
931
965
  id
932
966
  key
933
967
  name
934
- states { nodes { id name type } }
968
+ states(first: 50) { nodes { id name type } }
935
969
  }
936
970
  }
937
971
  }
@@ -1015,6 +1049,25 @@ function mapState(node) {
1015
1049
  type: String(node.type ?? "")
1016
1050
  };
1017
1051
  }
1052
+ function linearCycleIsActive(cycle, now = Date.now()) {
1053
+ if (!cycle) return false;
1054
+ if (cycle.completedAt) return false;
1055
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
1056
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
1057
+ if (Number.isFinite(start) && now < start) return false;
1058
+ if (Number.isFinite(end) && now > end) return false;
1059
+ return true;
1060
+ }
1061
+ function mapCycle(node) {
1062
+ if (!node?.name && node?.number == null) return null;
1063
+ const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
1064
+ if (!name) return null;
1065
+ return {
1066
+ name,
1067
+ number: typeof node.number === "number" ? node.number : void 0,
1068
+ isActive: linearCycleIsActive(node)
1069
+ };
1070
+ }
1018
1071
  function mapIssue(node) {
1019
1072
  const team = node.team;
1020
1073
  return {
@@ -1032,7 +1085,8 @@ function mapIssue(node) {
1032
1085
  name: String(team.name ?? ""),
1033
1086
  states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
1034
1087
  } : void 0,
1035
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
1088
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
1089
+ cycle: mapCycle(node.cycle)
1036
1090
  };
1037
1091
  }
1038
1092
  function toIssueInfo(issue) {
@@ -1042,7 +1096,11 @@ function toIssueInfo(issue) {
1042
1096
  title: issue.title,
1043
1097
  url: issue.url,
1044
1098
  labels: issue.labels,
1045
- provider: "linear"
1099
+ provider: "linear",
1100
+ assignee: issue.assignee?.name,
1101
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
1102
+ cycle: issue.cycle ?? null,
1103
+ teamKey: issue.team?.key || void 0
1046
1104
  };
1047
1105
  }
1048
1106
  function mapTeam(node) {
@@ -1084,10 +1142,21 @@ function normalizePriority(priority) {
1084
1142
  }
1085
1143
  return priority;
1086
1144
  }
1087
- async function listLinearIssuesDirect(opts) {
1088
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
1145
+ async function listLinearAssignedIssues(opts) {
1146
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
1089
1147
  const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
1090
- return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
1148
+ return {
1149
+ viewer: {
1150
+ id: String(json.viewer?.id ?? ""),
1151
+ name: String(json.viewer?.name ?? "")
1152
+ },
1153
+ issues: (json.viewer?.assignedIssues?.nodes ?? []).map(
1154
+ (node) => toIssueInfo(mapIssue(node))
1155
+ )
1156
+ };
1157
+ }
1158
+ async function listLinearIssuesDirect(opts) {
1159
+ return (await listLinearAssignedIssues(opts)).issues;
1091
1160
  }
1092
1161
  async function listLinearTeams(opts) {
1093
1162
  const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
@@ -1206,13 +1275,13 @@ async function validateLinearApiKey(apiKey) {
1206
1275
 
1207
1276
  // src/integrations/issues.ts
1208
1277
  async function listGitHubIssues(repoPath, opts) {
1209
- const limit = Math.max(1, Math.min(100, opts?.limit ?? 50));
1278
+ const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
1210
1279
  const slug = await resolveGithubRepoSlug(repoPath);
1211
1280
  const args = [
1212
1281
  "issue",
1213
1282
  "list",
1214
1283
  "--json",
1215
- "number,title,url,labels",
1284
+ "number,title,url,labels,assignees",
1216
1285
  "--limit",
1217
1286
  String(limit),
1218
1287
  "--state",
@@ -1235,27 +1304,55 @@ async function listGitHubIssues(repoPath, opts) {
1235
1304
  const labels = (item.labels ?? []).map(
1236
1305
  (l) => typeof l === "string" ? l : String(l?.name ?? "")
1237
1306
  ).filter(Boolean);
1307
+ const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
1238
1308
  return {
1239
1309
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
1240
1310
  identifier,
1241
1311
  title: String(item.title ?? ""),
1242
1312
  url: String(item.url ?? ""),
1243
1313
  labels,
1244
- provider: "github"
1314
+ provider: "github",
1315
+ assignee: assignees[0],
1316
+ assignees
1245
1317
  };
1246
1318
  });
1247
1319
  }
1320
+ async function githubViewerLogin(repoPath) {
1321
+ const { stdout, exitCode } = await gh(["api", "user", "--jq", ".login"], repoPath, {
1322
+ reject: false
1323
+ });
1324
+ if (exitCode !== 0) return "";
1325
+ return stdout.trim();
1326
+ }
1248
1327
  async function listIssues(repoPath) {
1249
1328
  const settings = loadAppSettings();
1250
1329
  const preferredSource = settings.integrations.issueSource ?? "github";
1251
1330
  const linearConnected = isLinearConnected(settings);
1252
1331
  const source = resolveEffectiveIssueSource(settings);
1253
1332
  if (source === "linear") {
1254
- const issues2 = await listLinearIssuesDirect();
1255
- return { source, preferredSource, linearConnected, issues: issues2 };
1333
+ const listed = await listLinearAssignedIssues();
1334
+ return {
1335
+ source,
1336
+ preferredSource,
1337
+ linearConnected,
1338
+ issues: listed.issues,
1339
+ viewer: {
1340
+ login: listed.viewer.name,
1341
+ name: listed.viewer.name
1342
+ }
1343
+ };
1256
1344
  }
1257
- const issues = await listGitHubIssues(repoPath);
1258
- return { source, preferredSource, linearConnected, issues };
1345
+ const [issues, login] = await Promise.all([
1346
+ listGitHubIssues(repoPath),
1347
+ githubViewerLogin(repoPath)
1348
+ ]);
1349
+ return {
1350
+ source,
1351
+ preferredSource,
1352
+ linearConnected,
1353
+ issues,
1354
+ viewer: login ? { login } : void 0
1355
+ };
1259
1356
  }
1260
1357
 
1261
1358
  // src/composer/diff-comment.ts
@@ -1343,6 +1440,12 @@ function buildPastedTextAttachment(text4, opts) {
1343
1440
  }
1344
1441
 
1345
1442
  // src/plan/ask-user.ts
1443
+ function planQuestionsSignature(questions) {
1444
+ return questions.map((q) => {
1445
+ const opts = q.options.map((o) => o.label).join("");
1446
+ return `${q.question}${opts}`;
1447
+ }).join("");
1448
+ }
1346
1449
  function asRecord(v) {
1347
1450
  return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
1348
1451
  }
@@ -1400,12 +1503,22 @@ function extractPendingPlanQuestions(parts) {
1400
1503
  if (!questions.length) continue;
1401
1504
  return {
1402
1505
  id: p.id || `ask-${i}`,
1506
+ signature: planQuestionsSignature(questions),
1403
1507
  questions,
1404
1508
  source: p.name || "ask_user"
1405
1509
  };
1406
1510
  }
1407
1511
  return null;
1408
1512
  }
1513
+ function latestPendingPlanQuestions(input) {
1514
+ if (input.userReplied) return null;
1515
+ const fromLive = extractPendingPlanQuestions(input.liveParts);
1516
+ if (fromLive) return fromLive;
1517
+ const last = input.messages[input.messages.length - 1];
1518
+ if (last?.role === "user") return null;
1519
+ if (last?.role === "agent") return extractPendingPlanQuestions(last.parts);
1520
+ return null;
1521
+ }
1409
1522
  function formatPlanQuestionAnswers(questions, answers) {
1410
1523
  const lines = ["Answers to your questions:"];
1411
1524
  for (let i = 0; i < questions.length; i++) {
@@ -1444,6 +1557,7 @@ function formatPlanQuestionsForChat(questions) {
1444
1557
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1445
1558
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1446
1559
  import { z as z4 } from "zod";
1560
+ import { randomUUID as randomUUID3 } from "crypto";
1447
1561
  import { basename } from "path";
1448
1562
 
1449
1563
  // src/mcp/archive-guard.ts
@@ -2226,6 +2340,283 @@ function registerScheduleTools(server) {
2226
2340
  );
2227
2341
  }
2228
2342
 
2343
+ // src/board/load-home-board.ts
2344
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2345
+ import { join as join2 } from "path";
2346
+
2347
+ // src/board/board-pins.ts
2348
+ import { randomUUID as randomUUID2 } from "crypto";
2349
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2350
+ import { join } from "path";
2351
+ var FILE = "home-board-pins.json";
2352
+ var VERSION = 1;
2353
+ function pinsFile() {
2354
+ return join(appDataDir(), FILE);
2355
+ }
2356
+ function readDisk() {
2357
+ const path2 = pinsFile();
2358
+ if (!existsSync(path2)) return [];
2359
+ try {
2360
+ const raw = JSON.parse(readFileSync(path2, "utf8"));
2361
+ if (raw?.version !== VERSION || !Array.isArray(raw.items)) return [];
2362
+ return raw.items.filter((item) => item?.id && item.kind && item.ref);
2363
+ } catch {
2364
+ return [];
2365
+ }
2366
+ }
2367
+ function writeDisk(items) {
2368
+ mkdirSync(appDataDir(), { recursive: true });
2369
+ writeFileSync(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
2370
+ }
2371
+ function listBoardPins() {
2372
+ return readDisk();
2373
+ }
2374
+ function addBoardPin(input) {
2375
+ const ref = input.ref.trim();
2376
+ if (!ref) throw new Error("Board item needs a ref");
2377
+ const pin = {
2378
+ id: randomUUID2(),
2379
+ kind: input.kind,
2380
+ ref,
2381
+ repoPath: input.repoPath.trim(),
2382
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
2383
+ title: input.title?.trim() || ref,
2384
+ url: input.url,
2385
+ labels: input.labels,
2386
+ provider: input.provider,
2387
+ assignee: input.assignee,
2388
+ cycle: input.cycle,
2389
+ teamKey: input.teamKey,
2390
+ headRefName: input.headRefName,
2391
+ author: input.author,
2392
+ remoteState: "open",
2393
+ needsWorkspacePick: issueNeedsWorkspacePick(
2394
+ input.provider,
2395
+ input.workspaceCount ?? 1
2396
+ )
2397
+ };
2398
+ const items = listBoardPins();
2399
+ const identity = boardPinIdentity(pin);
2400
+ const existing = items.find((item) => boardPinIdentity(item) === identity);
2401
+ if (existing) {
2402
+ const merged = { ...existing, ...pin, id: existing.id, addedAt: existing.addedAt };
2403
+ writeDisk(items.map((item) => item.id === existing.id ? merged : item));
2404
+ return merged;
2405
+ }
2406
+ writeDisk([...items, pin]);
2407
+ return pin;
2408
+ }
2409
+ function removeBoardPin(id) {
2410
+ const items = listBoardPins();
2411
+ const next = items.filter((item) => item.id !== id);
2412
+ if (next.length === items.length) return false;
2413
+ writeDisk(next);
2414
+ return true;
2415
+ }
2416
+ function replaceBoardPins(items) {
2417
+ writeDisk(items);
2418
+ }
2419
+ function clearBoardPins() {
2420
+ try {
2421
+ unlinkSync(pinsFile());
2422
+ } catch {
2423
+ }
2424
+ }
2425
+
2426
+ // src/board/load-home-board.ts
2427
+ var CACHE_VERSION = 1;
2428
+ var memory = null;
2429
+ var inflight = null;
2430
+ function errText(err) {
2431
+ return err instanceof Error ? err.message : String(err);
2432
+ }
2433
+ function homeBoardWorkspaceKey(workspaces) {
2434
+ return workspaces.map((w) => w.path).filter(Boolean).sort().join("\n");
2435
+ }
2436
+ function cacheFile() {
2437
+ return join2(appDataDir(), "home-board-cache.json");
2438
+ }
2439
+ function emptyInputs() {
2440
+ return {
2441
+ issues: [],
2442
+ prs: [],
2443
+ issueSource: "github",
2444
+ viewerLogin: void 0,
2445
+ issueErrors: [],
2446
+ prErrors: []
2447
+ };
2448
+ }
2449
+ function asLoaded(entry, fromCache) {
2450
+ const pins = syncBoardPins(listBoardPins(), entry.inputs.issues, entry.inputs.prs);
2451
+ return { ...entry.inputs, fetchedAt: entry.fetchedAt, fromCache, pins };
2452
+ }
2453
+ function cacheStillFresh(entry, key, now) {
2454
+ return entry.workspaceKey === key && now - entry.fetchedAt < HOME_BOARD_CACHE_TTL_MS;
2455
+ }
2456
+ function shouldCacheHomeBoardInputs(inputs) {
2457
+ const issuesFailed = inputs.issues.length === 0 && inputs.issueErrors.length > 0;
2458
+ const prsFailed = inputs.prs.length === 0 && inputs.prErrors.length > 0;
2459
+ return !(issuesFailed && prsFailed);
2460
+ }
2461
+ function readDiskCache() {
2462
+ const path2 = cacheFile();
2463
+ if (!existsSync2(path2)) return null;
2464
+ try {
2465
+ const raw = JSON.parse(readFileSync2(path2, "utf8"));
2466
+ if (raw?.version !== CACHE_VERSION || typeof raw.fetchedAt !== "number") {
2467
+ return null;
2468
+ }
2469
+ if (!raw.inputs || !Array.isArray(raw.inputs.issues) || !Array.isArray(raw.inputs.prs)) {
2470
+ return null;
2471
+ }
2472
+ return raw;
2473
+ } catch {
2474
+ return null;
2475
+ }
2476
+ }
2477
+ function writeDiskCache(entry) {
2478
+ try {
2479
+ mkdirSync2(appDataDir(), { recursive: true });
2480
+ writeFileSync2(cacheFile(), JSON.stringify(entry), "utf8");
2481
+ } catch {
2482
+ }
2483
+ }
2484
+ function deleteDiskCache() {
2485
+ try {
2486
+ unlinkSync2(cacheFile());
2487
+ } catch {
2488
+ }
2489
+ }
2490
+ function resetHomeBoardMemory() {
2491
+ memory = null;
2492
+ inflight = null;
2493
+ }
2494
+ function clearHomeBoardCache() {
2495
+ resetHomeBoardMemory();
2496
+ deleteDiskCache();
2497
+ }
2498
+ async function loadHomeBoardInputs(workspaces) {
2499
+ const paths = workspaces.map((w) => w.path).filter(Boolean);
2500
+ if (paths.length === 0) return emptyInputs();
2501
+ const issueErrors = [];
2502
+ const prErrors = [];
2503
+ let issueSource = "github";
2504
+ let viewerLogin;
2505
+ let issues = [];
2506
+ try {
2507
+ const first = await listIssues(paths[0]);
2508
+ issueSource = first.source;
2509
+ viewerLogin = first.viewer?.login || first.viewer?.name || void 0;
2510
+ if (first.source === "linear") {
2511
+ const repoPath = paths[0] ?? "";
2512
+ issues = first.issues.map((issue) => ({
2513
+ ...issue,
2514
+ repoPath,
2515
+ needsWorkspacePick: issueNeedsWorkspacePick(
2516
+ issue.provider ?? first.source,
2517
+ paths.length
2518
+ )
2519
+ }));
2520
+ } else {
2521
+ const settled = await Promise.allSettled(
2522
+ paths.map(async (path2) => {
2523
+ const result = path2 === paths[0] ? first : await listIssues(path2);
2524
+ return result.issues.map((issue) => ({
2525
+ ...issue,
2526
+ repoPath: path2,
2527
+ needsWorkspacePick: issueNeedsWorkspacePick(
2528
+ issue.provider ?? result.source,
2529
+ 1
2530
+ )
2531
+ }));
2532
+ })
2533
+ );
2534
+ const collected = [];
2535
+ for (const item of settled) {
2536
+ if (item.status === "fulfilled") collected.push(...item.value);
2537
+ else issueErrors.push(errText(item.reason));
2538
+ }
2539
+ issues = collected;
2540
+ if (collected.length === 0 && issueErrors[0]) {
2541
+ throw new Error(issueErrors[0]);
2542
+ }
2543
+ }
2544
+ issues = dedupeBoardIssues(issues);
2545
+ } catch (err) {
2546
+ issueErrors.push(errText(err));
2547
+ issues = [];
2548
+ }
2549
+ let prs = [];
2550
+ try {
2551
+ const settled = await Promise.allSettled(
2552
+ paths.map(async (path2) => {
2553
+ const list = await listPrs(path2);
2554
+ return list.map((pr) => ({ ...pr, repoPath: path2 }));
2555
+ })
2556
+ );
2557
+ const collected = [];
2558
+ for (const item of settled) {
2559
+ if (item.status === "fulfilled") collected.push(...item.value);
2560
+ else prErrors.push(errText(item.reason));
2561
+ }
2562
+ if (collected.length === 0 && prErrors[0]) {
2563
+ throw new Error(prErrors[0]);
2564
+ }
2565
+ prs = dedupeBoardPrs(collected);
2566
+ } catch (err) {
2567
+ prErrors.push(errText(err));
2568
+ prs = [];
2569
+ }
2570
+ return { issues, prs, issueSource, viewerLogin, issueErrors, prErrors };
2571
+ }
2572
+ async function getHomeBoardInputs(workspaces, opts) {
2573
+ const key = homeBoardWorkspaceKey(workspaces);
2574
+ const now = opts?.now ?? Date.now();
2575
+ if (!key) {
2576
+ return {
2577
+ ...emptyInputs(),
2578
+ fetchedAt: now,
2579
+ fromCache: false,
2580
+ pins: listBoardPins()
2581
+ };
2582
+ }
2583
+ if (!opts?.refresh) {
2584
+ const disk = readDiskCache();
2585
+ const best = memory && disk ? disk.fetchedAt >= memory.fetchedAt ? disk : memory : memory ?? disk;
2586
+ if (best && cacheStillFresh(best, key, now)) {
2587
+ memory = best;
2588
+ return asLoaded(best, true);
2589
+ }
2590
+ }
2591
+ if (inflight && inflight.key === key) {
2592
+ return inflight.promise;
2593
+ }
2594
+ const promise = (async () => {
2595
+ const inputs = await loadHomeBoardInputs(workspaces);
2596
+ const fetchedAt = opts?.now ?? Date.now();
2597
+ const pins = syncBoardPins(listBoardPins(), inputs.issues, inputs.prs);
2598
+ replaceBoardPins(pins);
2599
+ const loaded = { ...inputs, fetchedAt, fromCache: false, pins };
2600
+ if (shouldCacheHomeBoardInputs(inputs)) {
2601
+ const entry = {
2602
+ version: CACHE_VERSION,
2603
+ workspaceKey: key,
2604
+ fetchedAt,
2605
+ inputs
2606
+ };
2607
+ memory = entry;
2608
+ writeDiskCache(entry);
2609
+ }
2610
+ return loaded;
2611
+ })();
2612
+ inflight = { key, promise };
2613
+ try {
2614
+ return await promise;
2615
+ } finally {
2616
+ if (inflight?.promise === promise) inflight = null;
2617
+ }
2618
+ }
2619
+
2229
2620
  // src/mcp/server.ts
2230
2621
  var MAX_ORCH_THREADS = 5;
2231
2622
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -2246,6 +2637,93 @@ function withTimeout(promise, ms, label) {
2246
2637
  );
2247
2638
  });
2248
2639
  }
2640
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
2641
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
2642
+ let parentId = args.parentThreadId?.trim() || "";
2643
+ let parent = parentId ? orch.getThread(parentId) : null;
2644
+ let parentCorrectedFrom;
2645
+ if (envParentId) {
2646
+ const envParent = orch.getThread(envParentId);
2647
+ if (envParent) {
2648
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
2649
+ else if (!parentId) parentCorrectedFrom = void 0;
2650
+ parentId = envParentId;
2651
+ parent = envParent;
2652
+ }
2653
+ }
2654
+ if (parentId && !parent) {
2655
+ parentCorrectedFrom = parentId;
2656
+ parentId = "";
2657
+ parent = null;
2658
+ }
2659
+ if (parentId) {
2660
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
2661
+ if (children.length >= MAX_ORCH_THREADS) {
2662
+ return {
2663
+ ok: false,
2664
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
2665
+ };
2666
+ }
2667
+ }
2668
+ let agentArg = args.agent;
2669
+ let agentCoercedFrom;
2670
+ const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
2671
+ if (resolvedProbe === "codex") {
2672
+ agentCoercedFrom = agentArg ?? "codex";
2673
+ const accountAgent = resolveNewThreadOptions2({}).agent;
2674
+ agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
2675
+ }
2676
+ const opts = resolveNewThreadOptions2({
2677
+ agent: agentArg,
2678
+ model: args.model
2679
+ });
2680
+ try {
2681
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
2682
+ const thread = await withTimeout(
2683
+ orch.createThread({
2684
+ sourceType: args.sourceType,
2685
+ sourceRef: args.sourceRef,
2686
+ agent: opts.agent,
2687
+ model: opts.model,
2688
+ effort: opts.effort,
2689
+ fast: opts.fast,
2690
+ repoPath: args.repoPath,
2691
+ title: args.title,
2692
+ parentThreadId: parentId || null,
2693
+ cowboy: args.cowboy || void 0,
2694
+ attachments: args.attachments
2695
+ }),
2696
+ CREATE_THREAD_TIMEOUT_MS,
2697
+ "create_thread"
2698
+ );
2699
+ const alreadyStarted = priorIds.has(thread.id);
2700
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
2701
+ return {
2702
+ ok: true,
2703
+ text: JSON.stringify({
2704
+ id: thread.id,
2705
+ title: thread.title,
2706
+ branchName: thread.branchName,
2707
+ worktreePath: thread.worktreePath,
2708
+ agent: thread.agent,
2709
+ model: thread.model,
2710
+ status: thread.status,
2711
+ cowboy: Boolean(thread.cowboy),
2712
+ link: `sideboard://thread/${thread.id}`,
2713
+ parentThreadId: thread.parentThreadId,
2714
+ ...alreadyStarted ? { alreadyStarted: true } : {},
2715
+ ...agentCoercedFrom ? {
2716
+ agentCoercedFrom,
2717
+ note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
2718
+ } : {},
2719
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
2720
+ })
2721
+ };
2722
+ } catch (err) {
2723
+ const message = err instanceof Error ? err.message : String(err);
2724
+ return { ok: false, text: `create_thread failed: ${message}` };
2725
+ }
2726
+ }
2249
2727
  async function startMcpServer() {
2250
2728
  const orch = getOrchestrator();
2251
2729
  try {
@@ -2257,7 +2735,7 @@ async function startMcpServer() {
2257
2735
  );
2258
2736
  }
2259
2737
  try {
2260
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-YFWV7MOG.js");
2738
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-6RPALX4J.js");
2261
2739
  orch.setMaxConcurrent(maxConcurrentAgents2());
2262
2740
  } catch {
2263
2741
  }
@@ -2277,7 +2755,7 @@ async function startMcpServer() {
2277
2755
  if (!worktreeProfile) {
2278
2756
  server.tool(
2279
2757
  "list_workspaces",
2280
- "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.",
2758
+ "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.",
2281
2759
  {},
2282
2760
  async () => {
2283
2761
  const workspaces = orch.listWorkspaces();
@@ -2311,6 +2789,51 @@ async function startMcpServer() {
2311
2789
  };
2312
2790
  }
2313
2791
  );
2792
+ server.tool(
2793
+ "list_board",
2794
+ "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.",
2795
+ {
2796
+ query: z4.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
2797
+ repoPath: z4.string().optional().describe("Limit to one workspace path from list_workspaces"),
2798
+ kind: z4.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
2799
+ column: z4.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
2800
+ "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
2801
+ ),
2802
+ limit: z4.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
2803
+ },
2804
+ async ({ query, repoPath, kind, column, limit }) => {
2805
+ const workspaces = orch.listWorkspaces();
2806
+ const all = orch.getThreads(true);
2807
+ const names = new Map(workspaces.map((w) => [w.path, w.name]));
2808
+ const snap = assembleHomeBoard({
2809
+ threads: all.filter((t) => t.status !== "archived"),
2810
+ query,
2811
+ repoPath,
2812
+ kind: kind ?? "all",
2813
+ column: column === "needs_you" ? "new" : column,
2814
+ limit,
2815
+ workspaceName: (path2) => names.get(path2) ?? ""
2816
+ });
2817
+ return {
2818
+ content: [
2819
+ {
2820
+ type: "text",
2821
+ text: JSON.stringify(
2822
+ {
2823
+ columns: snap.columns,
2824
+ hidden: snap.hidden,
2825
+ totals: snap.totals,
2826
+ columnDefs: BOARD_COLUMN_DEFS,
2827
+ hint: HOME_BOARD_AGENT_HINT
2828
+ },
2829
+ null,
2830
+ 2
2831
+ )
2832
+ }
2833
+ ]
2834
+ };
2835
+ }
2836
+ );
2314
2837
  server.tool(
2315
2838
  "get_thread",
2316
2839
  "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.",
@@ -2349,25 +2872,31 @@ async function startMcpServer() {
2349
2872
  }
2350
2873
  server.tool(
2351
2874
  "present_artifact",
2352
- "Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Do not also emit the same document as a chat html/markdown fence. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
2875
+ "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.",
2353
2876
  {
2354
2877
  title: z4.string().describe("Short title shown in the artifact pane header"),
2355
- type: z4.enum(["html", "svg", "markdown", "react"]).describe(
2356
- "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
2878
+ type: z4.enum(["html", "svg", "markdown", "react", "log"]).describe(
2879
+ "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
2357
2880
  ),
2358
2881
  content: z4.string().describe(
2359
- "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
2882
+ "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
2360
2883
  ),
2361
- artifact_id: z4.string().optional().describe("Stable id when updating the same artifact across turns")
2884
+ artifact_id: z4.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
2885
+ status: z4.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
2886
+ phase: z4.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
2887
+ mode: z4.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
2362
2888
  },
2363
- async ({ title, type, artifact_id }) => {
2889
+ async ({ title, type, artifact_id, status, phase, mode }) => {
2364
2890
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2365
2891
  const payload = {
2366
2892
  ok: true,
2367
2893
  artifact_id: id,
2368
2894
  title,
2369
2895
  type,
2370
- message: "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
2896
+ status,
2897
+ phase,
2898
+ mode: type === "log" ? mode ?? "append" : void 0,
2899
+ message: type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
2371
2900
  };
2372
2901
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
2373
2902
  }
@@ -2480,7 +3009,7 @@ async function startMcpServer() {
2480
3009
  registerLinearTools(server);
2481
3010
  registerScheduleTools(server);
2482
3011
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-X3JNPR42.js");
2483
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-YFWV7MOG.js");
3012
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-6RPALX4J.js");
2484
3013
  const accountDefaults = resolveThreadDefaults2();
2485
3014
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
2486
3015
  server.tool(
@@ -2523,7 +3052,7 @@ async function startMcpServer() {
2523
3052
  );
2524
3053
  server.tool(
2525
3054
  "create_thread",
2526
- `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.`,
3055
+ `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.`,
2527
3056
  {
2528
3057
  sourceType: z4.enum(["branch", "pr", "ticket"]),
2529
3058
  sourceRef: z4.string(),
@@ -2541,104 +3070,118 @@ async function startMcpServer() {
2541
3070
  )
2542
3071
  },
2543
3072
  async (args) => {
2544
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
2545
- let parentId = args.parentThreadId?.trim() || "";
2546
- let parent = parentId ? orch.getThread(parentId) : null;
2547
- let parentCorrectedFrom;
2548
- if (envParentId) {
2549
- const envParent = orch.getThread(envParentId);
2550
- if (envParent) {
2551
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
2552
- else if (!parentId) parentCorrectedFrom = void 0;
2553
- parentId = envParentId;
2554
- parent = envParent;
2555
- }
2556
- }
2557
- if (parentId && !parent) {
2558
- parentCorrectedFrom = parentId;
2559
- parentId = "";
2560
- parent = null;
2561
- }
2562
- if (parentId) {
2563
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
2564
- if (children.length >= MAX_ORCH_THREADS) {
2565
- return {
2566
- content: [
2567
- {
2568
- type: "text",
2569
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
2570
- }
2571
- ],
2572
- isError: true
2573
- };
2574
- }
2575
- }
2576
- let agentArg = args.agent;
2577
- let agentCoercedFrom;
2578
- const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
2579
- if (resolvedProbe === "codex") {
2580
- agentCoercedFrom = agentArg ?? "codex";
2581
- const accountAgent = resolveNewThreadOptions2({}).agent;
2582
- agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
2583
- }
2584
- const opts = resolveNewThreadOptions2({
2585
- agent: agentArg,
2586
- model: args.model
2587
- });
2588
- try {
2589
- const thread = await withTimeout(
2590
- orch.createThread({
2591
- sourceType: args.sourceType,
2592
- sourceRef: args.sourceRef,
2593
- agent: opts.agent,
2594
- model: opts.model,
2595
- effort: opts.effort,
2596
- fast: opts.fast,
2597
- repoPath: args.repoPath,
2598
- title: args.title,
2599
- parentThreadId: parentId || null,
2600
- cowboy: args.cowboy || void 0
2601
- }),
2602
- CREATE_THREAD_TIMEOUT_MS,
2603
- "create_thread"
2604
- );
2605
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
3073
+ const result = await createOrchChildThread(orch, args, resolveNewThreadOptions2);
3074
+ return {
3075
+ content: [{ type: "text", text: result.text }],
3076
+ ...result.ok ? {} : { isError: true }
3077
+ };
3078
+ }
3079
+ );
3080
+ server.tool(
3081
+ "start_board_card",
3082
+ "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.",
3083
+ {
3084
+ kind: z4.enum(["ticket", "pr", "branch"]),
3085
+ ref: z4.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
3086
+ repoPath: z4.string().describe("Workspace path from list_workspaces / list_board"),
3087
+ title: z4.string().optional()
3088
+ },
3089
+ async ({ kind, ref, repoPath, title }) => {
3090
+ const root = await resolveRepoRoot(repoPath);
3091
+ const existing = findLiveThreadForCreate(
3092
+ {
3093
+ sourceType: kind,
3094
+ sourceRef: ref.trim(),
3095
+ repoPath: canonicalizeRepoPath(root),
3096
+ title: title?.trim()
3097
+ },
3098
+ orch.getThreads(false).map((t) => ({
3099
+ ...t,
3100
+ repoPath: canonicalizeRepoPath(t.repoPath)
3101
+ }))
3102
+ );
3103
+ if (existing) {
2606
3104
  return {
2607
3105
  content: [
2608
3106
  {
2609
3107
  type: "text",
2610
3108
  text: JSON.stringify({
2611
- id: thread.id,
2612
- title: thread.title,
2613
- branchName: thread.branchName,
2614
- worktreePath: thread.worktreePath,
2615
- agent: thread.agent,
2616
- model: thread.model,
2617
- status: thread.status,
2618
- cowboy: Boolean(thread.cowboy),
2619
- link: `sideboard://thread/${thread.id}`,
2620
- parentThreadId: thread.parentThreadId,
2621
- ...agentCoercedFrom ? {
2622
- agentCoercedFrom,
2623
- note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
2624
- } : {},
2625
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
3109
+ alreadyStarted: true,
3110
+ id: existing.id,
3111
+ title: existing.title,
3112
+ status: existing.status,
3113
+ link: `sideboard://thread/${existing.id}`
2626
3114
  })
2627
3115
  }
2628
3116
  ]
2629
3117
  };
2630
- } catch (err) {
2631
- const message = err instanceof Error ? err.message : String(err);
3118
+ }
3119
+ const workspaces = orch.listWorkspaces();
3120
+ const loaded = await getHomeBoardInputs(workspaces);
3121
+ const pin = findBoardPin(loaded.pins, kind, ref, root);
3122
+ if (kind === "branch") {
3123
+ const sourceRef = pin?.ref === "default" ? "default" : pin?.ref ?? ref.trim();
3124
+ const result2 = await createOrchChildThread(
3125
+ orch,
3126
+ {
3127
+ sourceType: "branch",
3128
+ sourceRef,
3129
+ repoPath: root,
3130
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
3131
+ },
3132
+ resolveNewThreadOptions2
3133
+ );
2632
3134
  return {
2633
- content: [
2634
- {
2635
- type: "text",
2636
- text: `create_thread failed: ${message}`
2637
- }
2638
- ],
2639
- isError: true
3135
+ content: [{ type: "text", text: result2.text }],
3136
+ ...result2.ok ? {} : { isError: true }
2640
3137
  };
2641
3138
  }
3139
+ if (kind === "ticket") {
3140
+ const issue = findBoardIssue(loaded.issues, ref, root);
3141
+ const ident = issue?.identifier ?? ref.trim();
3142
+ const attachments = issue ? [
3143
+ {
3144
+ id: randomUUID3(),
3145
+ name: issue.identifier,
3146
+ kind: "issue",
3147
+ content: [
3148
+ `Linked issue: ${issue.identifier} \u2014 ${issue.title}`,
3149
+ issue.url ? `URL: ${issue.url}` : null
3150
+ ].filter(Boolean).join("\n")
3151
+ }
3152
+ ] : void 0;
3153
+ const result2 = await createOrchChildThread(
3154
+ orch,
3155
+ {
3156
+ sourceType: "ticket",
3157
+ sourceRef: ident,
3158
+ repoPath: root,
3159
+ title: title?.trim() || issue?.title,
3160
+ attachments
3161
+ },
3162
+ resolveNewThreadOptions2
3163
+ );
3164
+ return {
3165
+ content: [{ type: "text", text: result2.text }],
3166
+ ...result2.ok ? {} : { isError: true }
3167
+ };
3168
+ }
3169
+ const pr = findBoardPr(loaded.prs, ref, root);
3170
+ const number = pr ? String(pr.number) : ref.trim().replace(/^#/, "");
3171
+ const result = await createOrchChildThread(
3172
+ orch,
3173
+ {
3174
+ sourceType: "pr",
3175
+ sourceRef: number,
3176
+ repoPath: root,
3177
+ title: title?.trim() || pr?.title
3178
+ },
3179
+ resolveNewThreadOptions2
3180
+ );
3181
+ return {
3182
+ content: [{ type: "text", text: result.text }],
3183
+ ...result.ok ? {} : { isError: true }
3184
+ };
2642
3185
  }
2643
3186
  );
2644
3187
  server.tool(
@@ -3285,7 +3828,7 @@ async function startMcpServer() {
3285
3828
  );
3286
3829
  server.tool(
3287
3830
  "list_issues",
3288
- "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.",
3831
+ "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.",
3289
3832
  { repoPath: z4.string() },
3290
3833
  async ({ repoPath }) => {
3291
3834
  const root = await resolveRepoRoot(repoPath);
@@ -3627,14 +4170,14 @@ async function runCloudConnect(opts) {
3627
4170
  }
3628
4171
 
3629
4172
  // src/agents/user-mcp-config.ts
3630
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4173
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3631
4174
  import { homedir } from "os";
3632
- import { dirname, join } from "path";
4175
+ import { dirname, join as join3 } from "path";
3633
4176
  function userCursorMcpConfigPath() {
3634
- return join(homedir(), ".cursor", "mcp.json");
4177
+ return join3(homedir(), ".cursor", "mcp.json");
3635
4178
  }
3636
4179
  function userClaudeMcpConfigPath() {
3637
- return join(homedir(), ".claude.json");
4180
+ return join3(homedir(), ".claude.json");
3638
4181
  }
3639
4182
  function asObject(value) {
3640
4183
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
@@ -3661,16 +4204,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
3661
4204
  }
3662
4205
  function writeMergedMcpServersJson(configPath, sideboard) {
3663
4206
  let existing = {};
3664
- if (existsSync(configPath)) {
4207
+ if (existsSync3(configPath)) {
3665
4208
  try {
3666
- existing = JSON.parse(readFileSync(configPath, "utf8"));
4209
+ existing = JSON.parse(readFileSync3(configPath, "utf8"));
3667
4210
  } catch {
3668
4211
  existing = {};
3669
4212
  }
3670
4213
  }
3671
4214
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
3672
- mkdirSync(dirname(configPath), { recursive: true });
3673
- writeFileSync(configPath, `${JSON.stringify(next, null, 2)}
4215
+ mkdirSync3(dirname(configPath), { recursive: true });
4216
+ writeFileSync3(configPath, `${JSON.stringify(next, null, 2)}
3674
4217
  `);
3675
4218
  }
3676
4219
  function launchFromResolved(server) {
@@ -3688,7 +4231,7 @@ async function registerPackagedUserMcpClients() {
3688
4231
  const cursor = userCursorMcpConfigPath();
3689
4232
  writeMergedMcpServersJson(cursor, launch);
3690
4233
  const claude = userClaudeMcpConfigPath();
3691
- if (existsSync(claude)) {
4234
+ if (existsSync3(claude)) {
3692
4235
  writeMergedMcpServersJson(claude, launch);
3693
4236
  return { cursor, claude };
3694
4237
  }
@@ -5617,6 +6160,8 @@ export {
5617
6160
  GITHUB_GIT_AUTH_MODES,
5618
6161
  GLOBAL_WORKSPACE_ID,
5619
6162
  HARNESS_ENV_KEYS,
6163
+ HOME_BOARD_CACHE_TTL_MS,
6164
+ ISSUE_SOURCE_LABELS,
5620
6165
  LEGACY_ATTACHMENTS_DIR,
5621
6166
  LEGACY_PLAN_FILE_REL,
5622
6167
  LEGACY_REVIEW_REQUEST_PATH,
@@ -5658,6 +6203,7 @@ export {
5658
6203
  THINKING_EFFORTS,
5659
6204
  WORKTREE_MCP_TOOLS,
5660
6205
  ackSlackInboundSeen,
6206
+ addBoardPin,
5661
6207
  addPrStackLayer,
5662
6208
  addStackLayerFromThread,
5663
6209
  addWorkspace,
@@ -5711,15 +6257,19 @@ export {
5711
6257
  caffeinateWhileRunningEnabled,
5712
6258
  caffeinateWhileSchedulesEnabled,
5713
6259
  caffeinateWhileSlackListenEnabled,
6260
+ canonicalizeRepoPath,
5714
6261
  captureLoginEnv,
5715
6262
  captureTurnBaseline,
5716
6263
  checkoutPrStackLayer,
5717
6264
  childEnvWithAppSettings,
5718
6265
  claimDesktopHost,
6266
+ classifyWorktreeColumn,
5719
6267
  claudeAdapter,
5720
6268
  claudeChromeEnabled,
5721
6269
  claudeUserSettingsPath,
5722
6270
  cleanupOrphanWorktrees,
6271
+ clearBoardPins,
6272
+ clearHomeBoardCache,
5723
6273
  cloneRepoIntoSideboard,
5724
6274
  codexAdapter,
5725
6275
  codexSandboxWritableRootsArgs,
@@ -5792,6 +6342,7 @@ export {
5792
6342
  finalizeParts,
5793
6343
  findConventionSetup,
5794
6344
  findInvalidCacheControlTtlOrder,
6345
+ findLiveThreadForCreate,
5795
6346
  findOrphanWorktrees,
5796
6347
  findSlackCoordinator,
5797
6348
  findThreadByRef,
@@ -5843,6 +6394,7 @@ export {
5843
6394
  getGitHubStatus,
5844
6395
  getGithubGitAuthMode,
5845
6396
  getGithubPat,
6397
+ getHomeBoardInputs,
5846
6398
  getIssueSource,
5847
6399
  getLinearApiKey,
5848
6400
  getLinearAuthToken,
@@ -5865,6 +6417,7 @@ export {
5865
6417
  git,
5866
6418
  githubAgentGitEnv,
5867
6419
  globalAgentCwd,
6420
+ groupHomeBoardWorktrees,
5868
6421
  handleSlackInbound,
5869
6422
  harnessEnvKey,
5870
6423
  hasBakedLinearOAuth,
@@ -5898,9 +6451,11 @@ export {
5898
6451
  isGhRateLimitError,
5899
6452
  isGlobalRepoPath,
5900
6453
  isGlobalThread,
6454
+ isHomeBoardThread,
5901
6455
  isImageFilePath,
5902
6456
  isInPrStack,
5903
6457
  isInboundForThisDesktop,
6458
+ isIssueSourceConnected,
5904
6459
  isLinearConnected,
5905
6460
  isLinearOAuthCancelled,
5906
6461
  isOrchestratorCapableAgent,
@@ -5923,12 +6478,16 @@ export {
5923
6478
  isThreadCaffeinated,
5924
6479
  isThreadRecordFile,
5925
6480
  isWorkspaceScratchPath,
6481
+ issueSourceLabel,
5926
6482
  lastRequestOccupancy,
6483
+ latestPendingPlanQuestions,
5927
6484
  linearAuthorizationHeader,
6485
+ linearCycleIsActive,
5928
6486
  linearGraphql,
5929
6487
  linearOAuthAuthorizeUrl,
5930
6488
  linearOAuthCredentials,
5931
6489
  listAgentSetupInfo,
6490
+ listBoardPins,
5932
6491
  listBranchCommits,
5933
6492
  listBranches,
5934
6493
  listBrightsyAccounts,
@@ -5940,6 +6499,7 @@ export {
5940
6499
  listGitHubIssues,
5941
6500
  listGlobalThreads,
5942
6501
  listIssues,
6502
+ listLinearAssignedIssues,
5943
6503
  listLinearIssues,
5944
6504
  listLinearIssuesDirect,
5945
6505
  listLinearTeams,
@@ -5959,6 +6519,7 @@ export {
5959
6519
  loadAppSettings,
5960
6520
  loadBrightsyConfig,
5961
6521
  loadConductorSettings,
6522
+ loadHomeBoardInputs,
5962
6523
  loadRepoSettings,
5963
6524
  loadWorkspaceSettings,
5964
6525
  locksDir,
@@ -6007,6 +6568,7 @@ export {
6007
6568
  permissionMode,
6008
6569
  persistVaultKeyInKeychain,
6009
6570
  planFileAbs,
6571
+ planQuestionsSignature,
6010
6572
  pollSlackOutboundWatches,
6011
6573
  posixShellSingleQuote,
6012
6574
  preferredCursorCostCents,
@@ -6028,6 +6590,7 @@ export {
6028
6590
  registerPackagedUserMcpClients,
6029
6591
  releaseCaffeinateHoldForThread,
6030
6592
  releaseDesktopHost,
6593
+ removeBoardPin,
6031
6594
  removeWorkspace,
6032
6595
  removeWorktree,
6033
6596
  repoSlug,
@@ -6174,6 +6737,7 @@ export {
6174
6737
  withMaxOldSpaceSize,
6175
6738
  withThreadLock,
6176
6739
  workspaceSettingsSourceLabel,
6740
+ worktreeBoardStatus,
6177
6741
  worktreeCleanupSettings,
6178
6742
  worktreeDisplayLabel,
6179
6743
  worktreeDisplayLabelForGroup,