@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
@@ -2,46 +2,59 @@
2
2
 
3
3
  import {
4
4
  AGENT_GIT_ACTIONS,
5
+ BOARD_COLUMN_DEFS,
6
+ HOME_BOARD_AGENT_HINT,
7
+ HOME_BOARD_CACHE_TTL_MS,
8
+ assembleHomeBoard,
5
9
  createSchedule,
10
+ dedupeBoardIssues,
11
+ dedupeBoardPrs,
6
12
  deleteSchedule,
13
+ findBoardIssue,
14
+ findBoardPin,
15
+ findBoardPr,
16
+ findLiveThreadForCreate,
7
17
  fireSchedule,
8
18
  getOrchestrator,
9
19
  getSchedule,
20
+ issueNeedsWorkspacePick,
10
21
  listLinearIssues,
11
22
  listSchedules,
12
23
  listSlackOutboundWatches,
13
24
  listSlackWorkspaces,
25
+ pollSlackOutboundWatches,
14
26
  readTurnLive,
15
27
  recordSlackOutboundWatch,
16
- refreshSlackReplyBadges,
17
28
  requireSlackWorkspace,
18
29
  resolveScheduleThreadId,
19
30
  slackApi,
20
31
  slackTokenFor,
32
+ syncBoardPins,
21
33
  updateSchedule
22
- } from "../chunk-SDCPQL27.js";
34
+ } from "../chunk-DLM5LSQW.js";
23
35
  import {
24
36
  listModelsForAgent,
25
37
  sideboardMcpProfile
26
- } from "../chunk-JNAIKICO.js";
38
+ } from "../chunk-ZMUOPTKZ.js";
27
39
  import "../chunk-YMRT2DU6.js";
28
- import "../chunk-RBVVWBVB.js";
40
+ import "../chunk-SYLTXDRF.js";
29
41
  import "../chunk-N5PM7HGQ.js";
30
42
  import "../chunk-KLBOWJ74.js";
31
43
  import {
32
44
  GLOBAL_WORKSPACE_ID,
33
45
  isCloudCoordinatorThread
34
- } from "../chunk-O43IRQ2Y.js";
35
- import "../chunk-TRTWH6C2.js";
46
+ } from "../chunk-MNL4FSKY.js";
47
+ import "../chunk-RSFCB23A.js";
36
48
  import {
49
+ canonicalizeRepoPath,
37
50
  listBranches,
38
51
  listPrs,
39
52
  resolveGithubRepoSlug,
40
53
  resolveRepoRoot,
41
54
  warmGithubAgentAuth
42
- } from "../chunk-OHEFHIG3.js";
55
+ } from "../chunk-HRSBZIXS.js";
43
56
  import "../chunk-B3SJXYIJ.js";
44
- import "../chunk-RG737OWT.js";
57
+ import "../chunk-G6X6UFJO.js";
45
58
  import {
46
59
  gh
47
60
  } from "../chunk-KPIYENTF.js";
@@ -52,15 +65,18 @@ import {
52
65
  loadAppSettings,
53
66
  resolveEffectiveIssueSource,
54
67
  saveLinearOAuth
55
- } from "../chunk-BMIRX64H.js";
68
+ } from "../chunk-ZND6XV6J.js";
56
69
  import "../chunk-NSTQ6QKD.js";
57
70
  import "../chunk-KGZBYWZ3.js";
58
- import "../chunk-BD2ICC4D.js";
71
+ import {
72
+ appDataDir
73
+ } from "../chunk-BD2ICC4D.js";
59
74
 
60
75
  // src/mcp/server.ts
61
76
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
62
77
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
63
78
  import { z as z4 } from "zod";
79
+ import { randomUUID as randomUUID2 } from "crypto";
64
80
  import { basename } from "path";
65
81
 
66
82
  // src/http/fetch.ts
@@ -196,10 +212,13 @@ var ISSUE_FIELDS = `
196
212
  assignee { id name }
197
213
  team { id key name states { nodes { id name type } } }
198
214
  labels { nodes { name } }
215
+ cycle { id name number startsAt endsAt completedAt }
199
216
  `;
200
217
  var ASSIGNED_ISSUES_QUERY = `
201
218
  query SideboardAssignedIssues($first: Int!) {
202
219
  viewer {
220
+ id
221
+ name
203
222
  assignedIssues(
204
223
  first: $first
205
224
  orderBy: updatedAt
@@ -302,6 +321,25 @@ function mapState(node) {
302
321
  type: String(node.type ?? "")
303
322
  };
304
323
  }
324
+ function linearCycleIsActive(cycle, now = Date.now()) {
325
+ if (!cycle) return false;
326
+ if (cycle.completedAt) return false;
327
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
328
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
329
+ if (Number.isFinite(start) && now < start) return false;
330
+ if (Number.isFinite(end) && now > end) return false;
331
+ return true;
332
+ }
333
+ function mapCycle(node) {
334
+ if (!node?.name && node?.number == null) return null;
335
+ const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
336
+ if (!name) return null;
337
+ return {
338
+ name,
339
+ number: typeof node.number === "number" ? node.number : void 0,
340
+ isActive: linearCycleIsActive(node)
341
+ };
342
+ }
305
343
  function mapIssue(node) {
306
344
  const team = node.team;
307
345
  return {
@@ -319,7 +357,8 @@ function mapIssue(node) {
319
357
  name: String(team.name ?? ""),
320
358
  states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
321
359
  } : void 0,
322
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
360
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
361
+ cycle: mapCycle(node.cycle)
323
362
  };
324
363
  }
325
364
  function toIssueInfo(issue) {
@@ -329,7 +368,11 @@ function toIssueInfo(issue) {
329
368
  title: issue.title,
330
369
  url: issue.url,
331
370
  labels: issue.labels,
332
- provider: "linear"
371
+ provider: "linear",
372
+ assignee: issue.assignee?.name,
373
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
374
+ cycle: issue.cycle ?? null,
375
+ teamKey: issue.team?.key || void 0
333
376
  };
334
377
  }
335
378
  function mapTeam(node) {
@@ -371,10 +414,18 @@ function normalizePriority(priority) {
371
414
  }
372
415
  return priority;
373
416
  }
374
- async function listLinearIssuesDirect(opts) {
375
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
417
+ async function listLinearAssignedIssues(opts) {
418
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
376
419
  const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
377
- return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
420
+ return {
421
+ viewer: {
422
+ id: String(json.viewer?.id ?? ""),
423
+ name: String(json.viewer?.name ?? "")
424
+ },
425
+ issues: (json.viewer?.assignedIssues?.nodes ?? []).map(
426
+ (node) => toIssueInfo(mapIssue(node))
427
+ )
428
+ };
378
429
  }
379
430
  async function listLinearTeams(opts) {
380
431
  const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
@@ -483,13 +534,13 @@ async function commentLinearIssue(input, opts) {
483
534
 
484
535
  // src/integrations/issues.ts
485
536
  async function listGitHubIssues(repoPath, opts) {
486
- const limit = Math.max(1, Math.min(100, opts?.limit ?? 50));
537
+ const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
487
538
  const slug = await resolveGithubRepoSlug(repoPath);
488
539
  const args = [
489
540
  "issue",
490
541
  "list",
491
542
  "--json",
492
- "number,title,url,labels",
543
+ "number,title,url,labels,assignees",
493
544
  "--limit",
494
545
  String(limit),
495
546
  "--state",
@@ -512,27 +563,55 @@ async function listGitHubIssues(repoPath, opts) {
512
563
  const labels = (item.labels ?? []).map(
513
564
  (l) => typeof l === "string" ? l : String(l?.name ?? "")
514
565
  ).filter(Boolean);
566
+ const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
515
567
  return {
516
568
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
517
569
  identifier,
518
570
  title: String(item.title ?? ""),
519
571
  url: String(item.url ?? ""),
520
572
  labels,
521
- provider: "github"
573
+ provider: "github",
574
+ assignee: assignees[0],
575
+ assignees
522
576
  };
523
577
  });
524
578
  }
579
+ async function githubViewerLogin(repoPath) {
580
+ const { stdout, exitCode } = await gh(["api", "user", "--jq", ".login"], repoPath, {
581
+ reject: false
582
+ });
583
+ if (exitCode !== 0) return "";
584
+ return stdout.trim();
585
+ }
525
586
  async function listIssues(repoPath) {
526
587
  const settings = loadAppSettings();
527
588
  const preferredSource = settings.integrations.issueSource ?? "github";
528
589
  const linearConnected = isLinearConnected(settings);
529
590
  const source = resolveEffectiveIssueSource(settings);
530
591
  if (source === "linear") {
531
- const issues2 = await listLinearIssuesDirect();
532
- return { source, preferredSource, linearConnected, issues: issues2 };
592
+ const listed = await listLinearAssignedIssues();
593
+ return {
594
+ source,
595
+ preferredSource,
596
+ linearConnected,
597
+ issues: listed.issues,
598
+ viewer: {
599
+ login: listed.viewer.name,
600
+ name: listed.viewer.name
601
+ }
602
+ };
533
603
  }
534
- const issues = await listGitHubIssues(repoPath);
535
- return { source, preferredSource, linearConnected, issues };
604
+ const [issues, login] = await Promise.all([
605
+ listGitHubIssues(repoPath),
606
+ githubViewerLogin(repoPath)
607
+ ]);
608
+ return {
609
+ source,
610
+ preferredSource,
611
+ linearConnected,
612
+ issues,
613
+ viewer: login ? { login } : void 0
614
+ };
536
615
  }
537
616
 
538
617
  // src/mcp/archive-guard.ts
@@ -1027,7 +1106,7 @@ function registerSlackTools(server) {
1027
1106
  },
1028
1107
  async ({ team_id }) => {
1029
1108
  try {
1030
- await refreshSlackReplyBadges({ force: true });
1109
+ await pollSlackOutboundWatches({ force: true });
1031
1110
  const team = team_id?.trim();
1032
1111
  const watches = listSlackOutboundWatches().filter(
1033
1112
  (w) => !team || w.teamId === team
@@ -1315,6 +1394,221 @@ function registerScheduleTools(server) {
1315
1394
  );
1316
1395
  }
1317
1396
 
1397
+ // src/board/load-home-board.ts
1398
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
1399
+ import { join as join2 } from "path";
1400
+
1401
+ // src/board/board-pins.ts
1402
+ import { randomUUID } from "crypto";
1403
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
1404
+ import { join } from "path";
1405
+ var FILE = "home-board-pins.json";
1406
+ var VERSION = 1;
1407
+ function pinsFile() {
1408
+ return join(appDataDir(), FILE);
1409
+ }
1410
+ function readDisk() {
1411
+ const path = pinsFile();
1412
+ if (!existsSync(path)) return [];
1413
+ try {
1414
+ const raw = JSON.parse(readFileSync(path, "utf8"));
1415
+ if (raw?.version !== VERSION || !Array.isArray(raw.items)) return [];
1416
+ return raw.items.filter((item) => item?.id && item.kind && item.ref);
1417
+ } catch {
1418
+ return [];
1419
+ }
1420
+ }
1421
+ function writeDisk(items) {
1422
+ mkdirSync(appDataDir(), { recursive: true });
1423
+ writeFileSync(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
1424
+ }
1425
+ function listBoardPins() {
1426
+ return readDisk();
1427
+ }
1428
+ function replaceBoardPins(items) {
1429
+ writeDisk(items);
1430
+ }
1431
+
1432
+ // src/board/load-home-board.ts
1433
+ var CACHE_VERSION = 1;
1434
+ var memory = null;
1435
+ var inflight = null;
1436
+ function errText(err) {
1437
+ return err instanceof Error ? err.message : String(err);
1438
+ }
1439
+ function homeBoardWorkspaceKey(workspaces) {
1440
+ return workspaces.map((w) => w.path).filter(Boolean).sort().join("\n");
1441
+ }
1442
+ function cacheFile() {
1443
+ return join2(appDataDir(), "home-board-cache.json");
1444
+ }
1445
+ function emptyInputs() {
1446
+ return {
1447
+ issues: [],
1448
+ prs: [],
1449
+ issueSource: "github",
1450
+ viewerLogin: void 0,
1451
+ issueErrors: [],
1452
+ prErrors: []
1453
+ };
1454
+ }
1455
+ function asLoaded(entry, fromCache) {
1456
+ const pins = syncBoardPins(listBoardPins(), entry.inputs.issues, entry.inputs.prs);
1457
+ return { ...entry.inputs, fetchedAt: entry.fetchedAt, fromCache, pins };
1458
+ }
1459
+ function cacheStillFresh(entry, key, now) {
1460
+ return entry.workspaceKey === key && now - entry.fetchedAt < HOME_BOARD_CACHE_TTL_MS;
1461
+ }
1462
+ function shouldCacheHomeBoardInputs(inputs) {
1463
+ const issuesFailed = inputs.issues.length === 0 && inputs.issueErrors.length > 0;
1464
+ const prsFailed = inputs.prs.length === 0 && inputs.prErrors.length > 0;
1465
+ return !(issuesFailed && prsFailed);
1466
+ }
1467
+ function readDiskCache() {
1468
+ const path = cacheFile();
1469
+ if (!existsSync2(path)) return null;
1470
+ try {
1471
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
1472
+ if (raw?.version !== CACHE_VERSION || typeof raw.fetchedAt !== "number") {
1473
+ return null;
1474
+ }
1475
+ if (!raw.inputs || !Array.isArray(raw.inputs.issues) || !Array.isArray(raw.inputs.prs)) {
1476
+ return null;
1477
+ }
1478
+ return raw;
1479
+ } catch {
1480
+ return null;
1481
+ }
1482
+ }
1483
+ function writeDiskCache(entry) {
1484
+ try {
1485
+ mkdirSync2(appDataDir(), { recursive: true });
1486
+ writeFileSync2(cacheFile(), JSON.stringify(entry), "utf8");
1487
+ } catch {
1488
+ }
1489
+ }
1490
+ async function loadHomeBoardInputs(workspaces) {
1491
+ const paths = workspaces.map((w) => w.path).filter(Boolean);
1492
+ if (paths.length === 0) return emptyInputs();
1493
+ const issueErrors = [];
1494
+ const prErrors = [];
1495
+ let issueSource = "github";
1496
+ let viewerLogin;
1497
+ let issues = [];
1498
+ try {
1499
+ const first = await listIssues(paths[0]);
1500
+ issueSource = first.source;
1501
+ viewerLogin = first.viewer?.login || first.viewer?.name || void 0;
1502
+ if (first.source === "linear") {
1503
+ const repoPath = paths[0] ?? "";
1504
+ issues = first.issues.map((issue) => ({
1505
+ ...issue,
1506
+ repoPath,
1507
+ needsWorkspacePick: issueNeedsWorkspacePick(
1508
+ issue.provider ?? first.source,
1509
+ paths.length
1510
+ )
1511
+ }));
1512
+ } else {
1513
+ const settled = await Promise.allSettled(
1514
+ paths.map(async (path) => {
1515
+ const result = path === paths[0] ? first : await listIssues(path);
1516
+ return result.issues.map((issue) => ({
1517
+ ...issue,
1518
+ repoPath: path,
1519
+ needsWorkspacePick: issueNeedsWorkspacePick(
1520
+ issue.provider ?? result.source,
1521
+ 1
1522
+ )
1523
+ }));
1524
+ })
1525
+ );
1526
+ const collected = [];
1527
+ for (const item of settled) {
1528
+ if (item.status === "fulfilled") collected.push(...item.value);
1529
+ else issueErrors.push(errText(item.reason));
1530
+ }
1531
+ issues = collected;
1532
+ if (collected.length === 0 && issueErrors[0]) {
1533
+ throw new Error(issueErrors[0]);
1534
+ }
1535
+ }
1536
+ issues = dedupeBoardIssues(issues);
1537
+ } catch (err) {
1538
+ issueErrors.push(errText(err));
1539
+ issues = [];
1540
+ }
1541
+ let prs = [];
1542
+ try {
1543
+ const settled = await Promise.allSettled(
1544
+ paths.map(async (path) => {
1545
+ const list = await listPrs(path);
1546
+ return list.map((pr) => ({ ...pr, repoPath: path }));
1547
+ })
1548
+ );
1549
+ const collected = [];
1550
+ for (const item of settled) {
1551
+ if (item.status === "fulfilled") collected.push(...item.value);
1552
+ else prErrors.push(errText(item.reason));
1553
+ }
1554
+ if (collected.length === 0 && prErrors[0]) {
1555
+ throw new Error(prErrors[0]);
1556
+ }
1557
+ prs = dedupeBoardPrs(collected);
1558
+ } catch (err) {
1559
+ prErrors.push(errText(err));
1560
+ prs = [];
1561
+ }
1562
+ return { issues, prs, issueSource, viewerLogin, issueErrors, prErrors };
1563
+ }
1564
+ async function getHomeBoardInputs(workspaces, opts) {
1565
+ const key = homeBoardWorkspaceKey(workspaces);
1566
+ const now = opts?.now ?? Date.now();
1567
+ if (!key) {
1568
+ return {
1569
+ ...emptyInputs(),
1570
+ fetchedAt: now,
1571
+ fromCache: false,
1572
+ pins: listBoardPins()
1573
+ };
1574
+ }
1575
+ if (!opts?.refresh) {
1576
+ const disk = readDiskCache();
1577
+ const best = memory && disk ? disk.fetchedAt >= memory.fetchedAt ? disk : memory : memory ?? disk;
1578
+ if (best && cacheStillFresh(best, key, now)) {
1579
+ memory = best;
1580
+ return asLoaded(best, true);
1581
+ }
1582
+ }
1583
+ if (inflight && inflight.key === key) {
1584
+ return inflight.promise;
1585
+ }
1586
+ const promise = (async () => {
1587
+ const inputs = await loadHomeBoardInputs(workspaces);
1588
+ const fetchedAt = opts?.now ?? Date.now();
1589
+ const pins = syncBoardPins(listBoardPins(), inputs.issues, inputs.prs);
1590
+ replaceBoardPins(pins);
1591
+ const loaded = { ...inputs, fetchedAt, fromCache: false, pins };
1592
+ if (shouldCacheHomeBoardInputs(inputs)) {
1593
+ const entry = {
1594
+ version: CACHE_VERSION,
1595
+ workspaceKey: key,
1596
+ fetchedAt,
1597
+ inputs
1598
+ };
1599
+ memory = entry;
1600
+ writeDiskCache(entry);
1601
+ }
1602
+ return loaded;
1603
+ })();
1604
+ inflight = { key, promise };
1605
+ try {
1606
+ return await promise;
1607
+ } finally {
1608
+ if (inflight?.promise === promise) inflight = null;
1609
+ }
1610
+ }
1611
+
1318
1612
  // src/mcp/server.ts
1319
1613
  var MAX_ORCH_THREADS = 5;
1320
1614
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -1335,6 +1629,93 @@ function withTimeout(promise, ms, label) {
1335
1629
  );
1336
1630
  });
1337
1631
  }
1632
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
1633
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
1634
+ let parentId = args.parentThreadId?.trim() || "";
1635
+ let parent = parentId ? orch.getThread(parentId) : null;
1636
+ let parentCorrectedFrom;
1637
+ if (envParentId) {
1638
+ const envParent = orch.getThread(envParentId);
1639
+ if (envParent) {
1640
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
1641
+ else if (!parentId) parentCorrectedFrom = void 0;
1642
+ parentId = envParentId;
1643
+ parent = envParent;
1644
+ }
1645
+ }
1646
+ if (parentId && !parent) {
1647
+ parentCorrectedFrom = parentId;
1648
+ parentId = "";
1649
+ parent = null;
1650
+ }
1651
+ if (parentId) {
1652
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
1653
+ if (children.length >= MAX_ORCH_THREADS) {
1654
+ return {
1655
+ ok: false,
1656
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
1657
+ };
1658
+ }
1659
+ }
1660
+ let agentArg = args.agent;
1661
+ let agentCoercedFrom;
1662
+ const resolvedProbe = resolveNewThreadOptions({ agent: agentArg }).agent;
1663
+ if (resolvedProbe === "codex") {
1664
+ agentCoercedFrom = agentArg ?? "codex";
1665
+ const accountAgent = resolveNewThreadOptions({}).agent;
1666
+ agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
1667
+ }
1668
+ const opts = resolveNewThreadOptions({
1669
+ agent: agentArg,
1670
+ model: args.model
1671
+ });
1672
+ try {
1673
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
1674
+ const thread = await withTimeout(
1675
+ orch.createThread({
1676
+ sourceType: args.sourceType,
1677
+ sourceRef: args.sourceRef,
1678
+ agent: opts.agent,
1679
+ model: opts.model,
1680
+ effort: opts.effort,
1681
+ fast: opts.fast,
1682
+ repoPath: args.repoPath,
1683
+ title: args.title,
1684
+ parentThreadId: parentId || null,
1685
+ cowboy: args.cowboy || void 0,
1686
+ attachments: args.attachments
1687
+ }),
1688
+ CREATE_THREAD_TIMEOUT_MS,
1689
+ "create_thread"
1690
+ );
1691
+ const alreadyStarted = priorIds.has(thread.id);
1692
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
1693
+ return {
1694
+ ok: true,
1695
+ text: JSON.stringify({
1696
+ id: thread.id,
1697
+ title: thread.title,
1698
+ branchName: thread.branchName,
1699
+ worktreePath: thread.worktreePath,
1700
+ agent: thread.agent,
1701
+ model: thread.model,
1702
+ status: thread.status,
1703
+ cowboy: Boolean(thread.cowboy),
1704
+ link: `sideboard://thread/${thread.id}`,
1705
+ parentThreadId: thread.parentThreadId,
1706
+ ...alreadyStarted ? { alreadyStarted: true } : {},
1707
+ ...agentCoercedFrom ? {
1708
+ agentCoercedFrom,
1709
+ note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
1710
+ } : {},
1711
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
1712
+ })
1713
+ };
1714
+ } catch (err) {
1715
+ const message = err instanceof Error ? err.message : String(err);
1716
+ return { ok: false, text: `create_thread failed: ${message}` };
1717
+ }
1718
+ }
1338
1719
  async function startMcpServer() {
1339
1720
  const orch = getOrchestrator();
1340
1721
  try {
@@ -1346,7 +1727,7 @@ async function startMcpServer() {
1346
1727
  );
1347
1728
  }
1348
1729
  try {
1349
- const { maxConcurrentAgents } = await import("../app-settings-IKFWZDUK.js");
1730
+ const { maxConcurrentAgents } = await import("../app-settings-E7NEQY7D.js");
1350
1731
  orch.setMaxConcurrent(maxConcurrentAgents());
1351
1732
  } catch {
1352
1733
  }
@@ -1366,7 +1747,7 @@ async function startMcpServer() {
1366
1747
  if (!worktreeProfile) {
1367
1748
  server.tool(
1368
1749
  "list_workspaces",
1369
- "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.",
1750
+ "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.",
1370
1751
  {},
1371
1752
  async () => {
1372
1753
  const workspaces = orch.listWorkspaces();
@@ -1400,6 +1781,51 @@ async function startMcpServer() {
1400
1781
  };
1401
1782
  }
1402
1783
  );
1784
+ server.tool(
1785
+ "list_board",
1786
+ "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.",
1787
+ {
1788
+ query: z4.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
1789
+ repoPath: z4.string().optional().describe("Limit to one workspace path from list_workspaces"),
1790
+ kind: z4.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
1791
+ column: z4.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
1792
+ "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
1793
+ ),
1794
+ limit: z4.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
1795
+ },
1796
+ async ({ query, repoPath, kind, column, limit }) => {
1797
+ const workspaces = orch.listWorkspaces();
1798
+ const all = orch.getThreads(true);
1799
+ const names = new Map(workspaces.map((w) => [w.path, w.name]));
1800
+ const snap = assembleHomeBoard({
1801
+ threads: all.filter((t) => t.status !== "archived"),
1802
+ query,
1803
+ repoPath,
1804
+ kind: kind ?? "all",
1805
+ column: column === "needs_you" ? "new" : column,
1806
+ limit,
1807
+ workspaceName: (path) => names.get(path) ?? ""
1808
+ });
1809
+ return {
1810
+ content: [
1811
+ {
1812
+ type: "text",
1813
+ text: JSON.stringify(
1814
+ {
1815
+ columns: snap.columns,
1816
+ hidden: snap.hidden,
1817
+ totals: snap.totals,
1818
+ columnDefs: BOARD_COLUMN_DEFS,
1819
+ hint: HOME_BOARD_AGENT_HINT
1820
+ },
1821
+ null,
1822
+ 2
1823
+ )
1824
+ }
1825
+ ]
1826
+ };
1827
+ }
1828
+ );
1403
1829
  server.tool(
1404
1830
  "get_thread",
1405
1831
  "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.",
@@ -1569,7 +1995,7 @@ async function startMcpServer() {
1569
1995
  registerLinearTools(server);
1570
1996
  registerScheduleTools(server);
1571
1997
  const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-NBRUWYXO.js");
1572
- const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-IKFWZDUK.js");
1998
+ const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-E7NEQY7D.js");
1573
1999
  const accountDefaults = resolveThreadDefaults();
1574
2000
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
1575
2001
  server.tool(
@@ -1612,7 +2038,7 @@ async function startMcpServer() {
1612
2038
  );
1613
2039
  server.tool(
1614
2040
  "create_thread",
1615
- `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.`,
2041
+ `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.`,
1616
2042
  {
1617
2043
  sourceType: z4.enum(["branch", "pr", "ticket"]),
1618
2044
  sourceRef: z4.string(),
@@ -1630,104 +2056,118 @@ async function startMcpServer() {
1630
2056
  )
1631
2057
  },
1632
2058
  async (args) => {
1633
- const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
1634
- let parentId = args.parentThreadId?.trim() || "";
1635
- let parent = parentId ? orch.getThread(parentId) : null;
1636
- let parentCorrectedFrom;
1637
- if (envParentId) {
1638
- const envParent = orch.getThread(envParentId);
1639
- if (envParent) {
1640
- if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
1641
- else if (!parentId) parentCorrectedFrom = void 0;
1642
- parentId = envParentId;
1643
- parent = envParent;
1644
- }
1645
- }
1646
- if (parentId && !parent) {
1647
- parentCorrectedFrom = parentId;
1648
- parentId = "";
1649
- parent = null;
1650
- }
1651
- if (parentId) {
1652
- const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
1653
- if (children.length >= MAX_ORCH_THREADS) {
1654
- return {
1655
- content: [
1656
- {
1657
- type: "text",
1658
- text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
1659
- }
1660
- ],
1661
- isError: true
1662
- };
1663
- }
1664
- }
1665
- let agentArg = args.agent;
1666
- let agentCoercedFrom;
1667
- const resolvedProbe = resolveNewThreadOptions({ agent: agentArg }).agent;
1668
- if (resolvedProbe === "codex") {
1669
- agentCoercedFrom = agentArg ?? "codex";
1670
- const accountAgent = resolveNewThreadOptions({}).agent;
1671
- agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
1672
- }
1673
- const opts = resolveNewThreadOptions({
1674
- agent: agentArg,
1675
- model: args.model
1676
- });
1677
- try {
1678
- const thread = await withTimeout(
1679
- orch.createThread({
1680
- sourceType: args.sourceType,
1681
- sourceRef: args.sourceRef,
1682
- agent: opts.agent,
1683
- model: opts.model,
1684
- effort: opts.effort,
1685
- fast: opts.fast,
1686
- repoPath: args.repoPath,
1687
- title: args.title,
1688
- parentThreadId: parentId || null,
1689
- cowboy: args.cowboy || void 0
1690
- }),
1691
- CREATE_THREAD_TIMEOUT_MS,
1692
- "create_thread"
1693
- );
1694
- const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
2059
+ const result = await createOrchChildThread(orch, args, resolveNewThreadOptions);
2060
+ return {
2061
+ content: [{ type: "text", text: result.text }],
2062
+ ...result.ok ? {} : { isError: true }
2063
+ };
2064
+ }
2065
+ );
2066
+ server.tool(
2067
+ "start_board_card",
2068
+ "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.",
2069
+ {
2070
+ kind: z4.enum(["ticket", "pr", "branch"]),
2071
+ ref: z4.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
2072
+ repoPath: z4.string().describe("Workspace path from list_workspaces / list_board"),
2073
+ title: z4.string().optional()
2074
+ },
2075
+ async ({ kind, ref, repoPath, title }) => {
2076
+ const root = await resolveRepoRoot(repoPath);
2077
+ const existing = findLiveThreadForCreate(
2078
+ {
2079
+ sourceType: kind,
2080
+ sourceRef: ref.trim(),
2081
+ repoPath: canonicalizeRepoPath(root),
2082
+ title: title?.trim()
2083
+ },
2084
+ orch.getThreads(false).map((t) => ({
2085
+ ...t,
2086
+ repoPath: canonicalizeRepoPath(t.repoPath)
2087
+ }))
2088
+ );
2089
+ if (existing) {
1695
2090
  return {
1696
2091
  content: [
1697
2092
  {
1698
2093
  type: "text",
1699
2094
  text: JSON.stringify({
1700
- id: thread.id,
1701
- title: thread.title,
1702
- branchName: thread.branchName,
1703
- worktreePath: thread.worktreePath,
1704
- agent: thread.agent,
1705
- model: thread.model,
1706
- status: thread.status,
1707
- cowboy: Boolean(thread.cowboy),
1708
- link: `sideboard://thread/${thread.id}`,
1709
- parentThreadId: thread.parentThreadId,
1710
- ...agentCoercedFrom ? {
1711
- agentCoercedFrom,
1712
- note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
1713
- } : {},
1714
- ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
2095
+ alreadyStarted: true,
2096
+ id: existing.id,
2097
+ title: existing.title,
2098
+ status: existing.status,
2099
+ link: `sideboard://thread/${existing.id}`
1715
2100
  })
1716
2101
  }
1717
2102
  ]
1718
2103
  };
1719
- } catch (err) {
1720
- const message = err instanceof Error ? err.message : String(err);
2104
+ }
2105
+ const workspaces = orch.listWorkspaces();
2106
+ const loaded = await getHomeBoardInputs(workspaces);
2107
+ const pin = findBoardPin(loaded.pins, kind, ref, root);
2108
+ if (kind === "branch") {
2109
+ const sourceRef = pin?.ref === "default" ? "default" : pin?.ref ?? ref.trim();
2110
+ const result2 = await createOrchChildThread(
2111
+ orch,
2112
+ {
2113
+ sourceType: "branch",
2114
+ sourceRef,
2115
+ repoPath: root,
2116
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
2117
+ },
2118
+ resolveNewThreadOptions
2119
+ );
1721
2120
  return {
1722
- content: [
1723
- {
1724
- type: "text",
1725
- text: `create_thread failed: ${message}`
1726
- }
1727
- ],
1728
- isError: true
2121
+ content: [{ type: "text", text: result2.text }],
2122
+ ...result2.ok ? {} : { isError: true }
2123
+ };
2124
+ }
2125
+ if (kind === "ticket") {
2126
+ const issue = findBoardIssue(loaded.issues, ref, root);
2127
+ const ident = issue?.identifier ?? ref.trim();
2128
+ const attachments = issue ? [
2129
+ {
2130
+ id: randomUUID2(),
2131
+ name: issue.identifier,
2132
+ kind: "issue",
2133
+ content: [
2134
+ `Linked issue: ${issue.identifier} \u2014 ${issue.title}`,
2135
+ issue.url ? `URL: ${issue.url}` : null
2136
+ ].filter(Boolean).join("\n")
2137
+ }
2138
+ ] : void 0;
2139
+ const result2 = await createOrchChildThread(
2140
+ orch,
2141
+ {
2142
+ sourceType: "ticket",
2143
+ sourceRef: ident,
2144
+ repoPath: root,
2145
+ title: title?.trim() || issue?.title,
2146
+ attachments
2147
+ },
2148
+ resolveNewThreadOptions
2149
+ );
2150
+ return {
2151
+ content: [{ type: "text", text: result2.text }],
2152
+ ...result2.ok ? {} : { isError: true }
1729
2153
  };
1730
2154
  }
2155
+ const pr = findBoardPr(loaded.prs, ref, root);
2156
+ const number = pr ? String(pr.number) : ref.trim().replace(/^#/, "");
2157
+ const result = await createOrchChildThread(
2158
+ orch,
2159
+ {
2160
+ sourceType: "pr",
2161
+ sourceRef: number,
2162
+ repoPath: root,
2163
+ title: title?.trim() || pr?.title
2164
+ },
2165
+ resolveNewThreadOptions
2166
+ );
2167
+ return {
2168
+ content: [{ type: "text", text: result.text }],
2169
+ ...result.ok ? {} : { isError: true }
2170
+ };
1731
2171
  }
1732
2172
  );
1733
2173
  server.tool(
@@ -2374,7 +2814,7 @@ async function startMcpServer() {
2374
2814
  );
2375
2815
  server.tool(
2376
2816
  "list_issues",
2377
- "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.",
2817
+ "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.",
2378
2818
  { repoPath: z4.string() },
2379
2819
  async ({ repoPath }) => {
2380
2820
  const root = await resolveRepoRoot(repoPath);