@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
@@ -2,11 +2,22 @@
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,
@@ -18,30 +29,32 @@ import {
18
29
  resolveScheduleThreadId,
19
30
  slackApi,
20
31
  slackTokenFor,
32
+ syncBoardPins,
21
33
  updateSchedule
22
- } from "../chunk-GKY2GR2J.js";
34
+ } from "../chunk-FMBLOOAO.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
@@ -185,6 +201,16 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
185
201
 
186
202
  // src/integrations/linear.ts
187
203
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
204
+ var LIST_ISSUE_FIELDS = `
205
+ id
206
+ identifier
207
+ title
208
+ url
209
+ assignee { id name }
210
+ team { id key }
211
+ labels(first: 10) { nodes { name } }
212
+ cycle { name number startsAt endsAt completedAt }
213
+ `;
188
214
  var ISSUE_FIELDS = `
189
215
  id
190
216
  identifier
@@ -194,18 +220,21 @@ var ISSUE_FIELDS = `
194
220
  priority
195
221
  state { id name type }
196
222
  assignee { id name }
197
- team { id key name states { nodes { id name type } } }
198
- labels { nodes { name } }
223
+ team { id key name states(first: 50) { nodes { id name type } } }
224
+ labels(first: 50) { nodes { name } }
225
+ cycle { name number startsAt endsAt completedAt }
199
226
  `;
200
227
  var ASSIGNED_ISSUES_QUERY = `
201
228
  query SideboardAssignedIssues($first: Int!) {
202
229
  viewer {
230
+ id
231
+ name
203
232
  assignedIssues(
204
233
  first: $first
205
234
  orderBy: updatedAt
206
235
  filter: { state: { type: { nin: ["completed", "canceled"] } } }
207
236
  ) {
208
- nodes { ${ISSUE_FIELDS} }
237
+ nodes { ${LIST_ISSUE_FIELDS} }
209
238
  }
210
239
  }
211
240
  }
@@ -218,7 +247,7 @@ query SideboardTeams {
218
247
  id
219
248
  key
220
249
  name
221
- states { nodes { id name type } }
250
+ states(first: 50) { nodes { id name type } }
222
251
  }
223
252
  }
224
253
  }
@@ -302,6 +331,25 @@ function mapState(node) {
302
331
  type: String(node.type ?? "")
303
332
  };
304
333
  }
334
+ function linearCycleIsActive(cycle, now = Date.now()) {
335
+ if (!cycle) return false;
336
+ if (cycle.completedAt) return false;
337
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
338
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
339
+ if (Number.isFinite(start) && now < start) return false;
340
+ if (Number.isFinite(end) && now > end) return false;
341
+ return true;
342
+ }
343
+ function mapCycle(node) {
344
+ if (!node?.name && node?.number == null) return null;
345
+ const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
346
+ if (!name) return null;
347
+ return {
348
+ name,
349
+ number: typeof node.number === "number" ? node.number : void 0,
350
+ isActive: linearCycleIsActive(node)
351
+ };
352
+ }
305
353
  function mapIssue(node) {
306
354
  const team = node.team;
307
355
  return {
@@ -319,7 +367,8 @@ function mapIssue(node) {
319
367
  name: String(team.name ?? ""),
320
368
  states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
321
369
  } : void 0,
322
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
370
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
371
+ cycle: mapCycle(node.cycle)
323
372
  };
324
373
  }
325
374
  function toIssueInfo(issue) {
@@ -329,7 +378,11 @@ function toIssueInfo(issue) {
329
378
  title: issue.title,
330
379
  url: issue.url,
331
380
  labels: issue.labels,
332
- provider: "linear"
381
+ provider: "linear",
382
+ assignee: issue.assignee?.name,
383
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
384
+ cycle: issue.cycle ?? null,
385
+ teamKey: issue.team?.key || void 0
333
386
  };
334
387
  }
335
388
  function mapTeam(node) {
@@ -371,10 +424,18 @@ function normalizePriority(priority) {
371
424
  }
372
425
  return priority;
373
426
  }
374
- async function listLinearIssuesDirect(opts) {
375
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
427
+ async function listLinearAssignedIssues(opts) {
428
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
376
429
  const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
377
- return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
430
+ return {
431
+ viewer: {
432
+ id: String(json.viewer?.id ?? ""),
433
+ name: String(json.viewer?.name ?? "")
434
+ },
435
+ issues: (json.viewer?.assignedIssues?.nodes ?? []).map(
436
+ (node) => toIssueInfo(mapIssue(node))
437
+ )
438
+ };
378
439
  }
379
440
  async function listLinearTeams(opts) {
380
441
  const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
@@ -483,13 +544,13 @@ async function commentLinearIssue(input, opts) {
483
544
 
484
545
  // src/integrations/issues.ts
485
546
  async function listGitHubIssues(repoPath, opts) {
486
- const limit = Math.max(1, Math.min(100, opts?.limit ?? 50));
547
+ const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
487
548
  const slug = await resolveGithubRepoSlug(repoPath);
488
549
  const args = [
489
550
  "issue",
490
551
  "list",
491
552
  "--json",
492
- "number,title,url,labels",
553
+ "number,title,url,labels,assignees",
493
554
  "--limit",
494
555
  String(limit),
495
556
  "--state",
@@ -512,27 +573,55 @@ async function listGitHubIssues(repoPath, opts) {
512
573
  const labels = (item.labels ?? []).map(
513
574
  (l) => typeof l === "string" ? l : String(l?.name ?? "")
514
575
  ).filter(Boolean);
576
+ const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
515
577
  return {
516
578
  id: Number.isFinite(number) ? `gh-${number}` : identifier,
517
579
  identifier,
518
580
  title: String(item.title ?? ""),
519
581
  url: String(item.url ?? ""),
520
582
  labels,
521
- provider: "github"
583
+ provider: "github",
584
+ assignee: assignees[0],
585
+ assignees
522
586
  };
523
587
  });
524
588
  }
589
+ async function githubViewerLogin(repoPath) {
590
+ const { stdout, exitCode } = await gh(["api", "user", "--jq", ".login"], repoPath, {
591
+ reject: false
592
+ });
593
+ if (exitCode !== 0) return "";
594
+ return stdout.trim();
595
+ }
525
596
  async function listIssues(repoPath) {
526
597
  const settings = loadAppSettings();
527
598
  const preferredSource = settings.integrations.issueSource ?? "github";
528
599
  const linearConnected = isLinearConnected(settings);
529
600
  const source = resolveEffectiveIssueSource(settings);
530
601
  if (source === "linear") {
531
- const issues2 = await listLinearIssuesDirect();
532
- return { source, preferredSource, linearConnected, issues: issues2 };
602
+ const listed = await listLinearAssignedIssues();
603
+ return {
604
+ source,
605
+ preferredSource,
606
+ linearConnected,
607
+ issues: listed.issues,
608
+ viewer: {
609
+ login: listed.viewer.name,
610
+ name: listed.viewer.name
611
+ }
612
+ };
533
613
  }
534
- const issues = await listGitHubIssues(repoPath);
535
- return { source, preferredSource, linearConnected, issues };
614
+ const [issues, login] = await Promise.all([
615
+ listGitHubIssues(repoPath),
616
+ githubViewerLogin(repoPath)
617
+ ]);
618
+ return {
619
+ source,
620
+ preferredSource,
621
+ linearConnected,
622
+ issues,
623
+ viewer: login ? { login } : void 0
624
+ };
536
625
  }
537
626
 
538
627
  // src/mcp/archive-guard.ts
@@ -1315,6 +1404,221 @@ function registerScheduleTools(server) {
1315
1404
  );
1316
1405
  }
1317
1406
 
1407
+ // src/board/load-home-board.ts
1408
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
1409
+ import { join as join2 } from "path";
1410
+
1411
+ // src/board/board-pins.ts
1412
+ import { randomUUID } from "crypto";
1413
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
1414
+ import { join } from "path";
1415
+ var FILE = "home-board-pins.json";
1416
+ var VERSION = 1;
1417
+ function pinsFile() {
1418
+ return join(appDataDir(), FILE);
1419
+ }
1420
+ function readDisk() {
1421
+ const path = pinsFile();
1422
+ if (!existsSync(path)) return [];
1423
+ try {
1424
+ const raw = JSON.parse(readFileSync(path, "utf8"));
1425
+ if (raw?.version !== VERSION || !Array.isArray(raw.items)) return [];
1426
+ return raw.items.filter((item) => item?.id && item.kind && item.ref);
1427
+ } catch {
1428
+ return [];
1429
+ }
1430
+ }
1431
+ function writeDisk(items) {
1432
+ mkdirSync(appDataDir(), { recursive: true });
1433
+ writeFileSync(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
1434
+ }
1435
+ function listBoardPins() {
1436
+ return readDisk();
1437
+ }
1438
+ function replaceBoardPins(items) {
1439
+ writeDisk(items);
1440
+ }
1441
+
1442
+ // src/board/load-home-board.ts
1443
+ var CACHE_VERSION = 1;
1444
+ var memory = null;
1445
+ var inflight = null;
1446
+ function errText(err) {
1447
+ return err instanceof Error ? err.message : String(err);
1448
+ }
1449
+ function homeBoardWorkspaceKey(workspaces) {
1450
+ return workspaces.map((w) => w.path).filter(Boolean).sort().join("\n");
1451
+ }
1452
+ function cacheFile() {
1453
+ return join2(appDataDir(), "home-board-cache.json");
1454
+ }
1455
+ function emptyInputs() {
1456
+ return {
1457
+ issues: [],
1458
+ prs: [],
1459
+ issueSource: "github",
1460
+ viewerLogin: void 0,
1461
+ issueErrors: [],
1462
+ prErrors: []
1463
+ };
1464
+ }
1465
+ function asLoaded(entry, fromCache) {
1466
+ const pins = syncBoardPins(listBoardPins(), entry.inputs.issues, entry.inputs.prs);
1467
+ return { ...entry.inputs, fetchedAt: entry.fetchedAt, fromCache, pins };
1468
+ }
1469
+ function cacheStillFresh(entry, key, now) {
1470
+ return entry.workspaceKey === key && now - entry.fetchedAt < HOME_BOARD_CACHE_TTL_MS;
1471
+ }
1472
+ function shouldCacheHomeBoardInputs(inputs) {
1473
+ const issuesFailed = inputs.issues.length === 0 && inputs.issueErrors.length > 0;
1474
+ const prsFailed = inputs.prs.length === 0 && inputs.prErrors.length > 0;
1475
+ return !(issuesFailed && prsFailed);
1476
+ }
1477
+ function readDiskCache() {
1478
+ const path = cacheFile();
1479
+ if (!existsSync2(path)) return null;
1480
+ try {
1481
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
1482
+ if (raw?.version !== CACHE_VERSION || typeof raw.fetchedAt !== "number") {
1483
+ return null;
1484
+ }
1485
+ if (!raw.inputs || !Array.isArray(raw.inputs.issues) || !Array.isArray(raw.inputs.prs)) {
1486
+ return null;
1487
+ }
1488
+ return raw;
1489
+ } catch {
1490
+ return null;
1491
+ }
1492
+ }
1493
+ function writeDiskCache(entry) {
1494
+ try {
1495
+ mkdirSync2(appDataDir(), { recursive: true });
1496
+ writeFileSync2(cacheFile(), JSON.stringify(entry), "utf8");
1497
+ } catch {
1498
+ }
1499
+ }
1500
+ async function loadHomeBoardInputs(workspaces) {
1501
+ const paths = workspaces.map((w) => w.path).filter(Boolean);
1502
+ if (paths.length === 0) return emptyInputs();
1503
+ const issueErrors = [];
1504
+ const prErrors = [];
1505
+ let issueSource = "github";
1506
+ let viewerLogin;
1507
+ let issues = [];
1508
+ try {
1509
+ const first = await listIssues(paths[0]);
1510
+ issueSource = first.source;
1511
+ viewerLogin = first.viewer?.login || first.viewer?.name || void 0;
1512
+ if (first.source === "linear") {
1513
+ const repoPath = paths[0] ?? "";
1514
+ issues = first.issues.map((issue) => ({
1515
+ ...issue,
1516
+ repoPath,
1517
+ needsWorkspacePick: issueNeedsWorkspacePick(
1518
+ issue.provider ?? first.source,
1519
+ paths.length
1520
+ )
1521
+ }));
1522
+ } else {
1523
+ const settled = await Promise.allSettled(
1524
+ paths.map(async (path) => {
1525
+ const result = path === paths[0] ? first : await listIssues(path);
1526
+ return result.issues.map((issue) => ({
1527
+ ...issue,
1528
+ repoPath: path,
1529
+ needsWorkspacePick: issueNeedsWorkspacePick(
1530
+ issue.provider ?? result.source,
1531
+ 1
1532
+ )
1533
+ }));
1534
+ })
1535
+ );
1536
+ const collected = [];
1537
+ for (const item of settled) {
1538
+ if (item.status === "fulfilled") collected.push(...item.value);
1539
+ else issueErrors.push(errText(item.reason));
1540
+ }
1541
+ issues = collected;
1542
+ if (collected.length === 0 && issueErrors[0]) {
1543
+ throw new Error(issueErrors[0]);
1544
+ }
1545
+ }
1546
+ issues = dedupeBoardIssues(issues);
1547
+ } catch (err) {
1548
+ issueErrors.push(errText(err));
1549
+ issues = [];
1550
+ }
1551
+ let prs = [];
1552
+ try {
1553
+ const settled = await Promise.allSettled(
1554
+ paths.map(async (path) => {
1555
+ const list = await listPrs(path);
1556
+ return list.map((pr) => ({ ...pr, repoPath: path }));
1557
+ })
1558
+ );
1559
+ const collected = [];
1560
+ for (const item of settled) {
1561
+ if (item.status === "fulfilled") collected.push(...item.value);
1562
+ else prErrors.push(errText(item.reason));
1563
+ }
1564
+ if (collected.length === 0 && prErrors[0]) {
1565
+ throw new Error(prErrors[0]);
1566
+ }
1567
+ prs = dedupeBoardPrs(collected);
1568
+ } catch (err) {
1569
+ prErrors.push(errText(err));
1570
+ prs = [];
1571
+ }
1572
+ return { issues, prs, issueSource, viewerLogin, issueErrors, prErrors };
1573
+ }
1574
+ async function getHomeBoardInputs(workspaces, opts) {
1575
+ const key = homeBoardWorkspaceKey(workspaces);
1576
+ const now = opts?.now ?? Date.now();
1577
+ if (!key) {
1578
+ return {
1579
+ ...emptyInputs(),
1580
+ fetchedAt: now,
1581
+ fromCache: false,
1582
+ pins: listBoardPins()
1583
+ };
1584
+ }
1585
+ if (!opts?.refresh) {
1586
+ const disk = readDiskCache();
1587
+ const best = memory && disk ? disk.fetchedAt >= memory.fetchedAt ? disk : memory : memory ?? disk;
1588
+ if (best && cacheStillFresh(best, key, now)) {
1589
+ memory = best;
1590
+ return asLoaded(best, true);
1591
+ }
1592
+ }
1593
+ if (inflight && inflight.key === key) {
1594
+ return inflight.promise;
1595
+ }
1596
+ const promise = (async () => {
1597
+ const inputs = await loadHomeBoardInputs(workspaces);
1598
+ const fetchedAt = opts?.now ?? Date.now();
1599
+ const pins = syncBoardPins(listBoardPins(), inputs.issues, inputs.prs);
1600
+ replaceBoardPins(pins);
1601
+ const loaded = { ...inputs, fetchedAt, fromCache: false, pins };
1602
+ if (shouldCacheHomeBoardInputs(inputs)) {
1603
+ const entry = {
1604
+ version: CACHE_VERSION,
1605
+ workspaceKey: key,
1606
+ fetchedAt,
1607
+ inputs
1608
+ };
1609
+ memory = entry;
1610
+ writeDiskCache(entry);
1611
+ }
1612
+ return loaded;
1613
+ })();
1614
+ inflight = { key, promise };
1615
+ try {
1616
+ return await promise;
1617
+ } finally {
1618
+ if (inflight?.promise === promise) inflight = null;
1619
+ }
1620
+ }
1621
+
1318
1622
  // src/mcp/server.ts
1319
1623
  var MAX_ORCH_THREADS = 5;
1320
1624
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -1335,6 +1639,93 @@ function withTimeout(promise, ms, label) {
1335
1639
  );
1336
1640
  });
1337
1641
  }
1642
+ async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
1643
+ const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
1644
+ let parentId = args.parentThreadId?.trim() || "";
1645
+ let parent = parentId ? orch.getThread(parentId) : null;
1646
+ let parentCorrectedFrom;
1647
+ if (envParentId) {
1648
+ const envParent = orch.getThread(envParentId);
1649
+ if (envParent) {
1650
+ if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
1651
+ else if (!parentId) parentCorrectedFrom = void 0;
1652
+ parentId = envParentId;
1653
+ parent = envParent;
1654
+ }
1655
+ }
1656
+ if (parentId && !parent) {
1657
+ parentCorrectedFrom = parentId;
1658
+ parentId = "";
1659
+ parent = null;
1660
+ }
1661
+ if (parentId) {
1662
+ const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
1663
+ if (children.length >= MAX_ORCH_THREADS) {
1664
+ return {
1665
+ ok: false,
1666
+ text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
1667
+ };
1668
+ }
1669
+ }
1670
+ let agentArg = args.agent;
1671
+ let agentCoercedFrom;
1672
+ const resolvedProbe = resolveNewThreadOptions({ agent: agentArg }).agent;
1673
+ if (resolvedProbe === "codex") {
1674
+ agentCoercedFrom = agentArg ?? "codex";
1675
+ const accountAgent = resolveNewThreadOptions({}).agent;
1676
+ agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
1677
+ }
1678
+ const opts = resolveNewThreadOptions({
1679
+ agent: agentArg,
1680
+ model: args.model
1681
+ });
1682
+ try {
1683
+ const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
1684
+ const thread = await withTimeout(
1685
+ orch.createThread({
1686
+ sourceType: args.sourceType,
1687
+ sourceRef: args.sourceRef,
1688
+ agent: opts.agent,
1689
+ model: opts.model,
1690
+ effort: opts.effort,
1691
+ fast: opts.fast,
1692
+ repoPath: args.repoPath,
1693
+ title: args.title,
1694
+ parentThreadId: parentId || null,
1695
+ cowboy: args.cowboy || void 0,
1696
+ attachments: args.attachments
1697
+ }),
1698
+ CREATE_THREAD_TIMEOUT_MS,
1699
+ "create_thread"
1700
+ );
1701
+ const alreadyStarted = priorIds.has(thread.id);
1702
+ const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
1703
+ return {
1704
+ ok: true,
1705
+ text: JSON.stringify({
1706
+ id: thread.id,
1707
+ title: thread.title,
1708
+ branchName: thread.branchName,
1709
+ worktreePath: thread.worktreePath,
1710
+ agent: thread.agent,
1711
+ model: thread.model,
1712
+ status: thread.status,
1713
+ cowboy: Boolean(thread.cowboy),
1714
+ link: `sideboard://thread/${thread.id}`,
1715
+ parentThreadId: thread.parentThreadId,
1716
+ ...alreadyStarted ? { alreadyStarted: true } : {},
1717
+ ...agentCoercedFrom ? {
1718
+ agentCoercedFrom,
1719
+ note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
1720
+ } : {},
1721
+ ...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
1722
+ })
1723
+ };
1724
+ } catch (err) {
1725
+ const message = err instanceof Error ? err.message : String(err);
1726
+ return { ok: false, text: `create_thread failed: ${message}` };
1727
+ }
1728
+ }
1338
1729
  async function startMcpServer() {
1339
1730
  const orch = getOrchestrator();
1340
1731
  try {
@@ -1346,7 +1737,7 @@ async function startMcpServer() {
1346
1737
  );
1347
1738
  }
1348
1739
  try {
1349
- const { maxConcurrentAgents } = await import("../app-settings-IKFWZDUK.js");
1740
+ const { maxConcurrentAgents } = await import("../app-settings-E7NEQY7D.js");
1350
1741
  orch.setMaxConcurrent(maxConcurrentAgents());
1351
1742
  } catch {
1352
1743
  }
@@ -1366,7 +1757,7 @@ async function startMcpServer() {
1366
1757
  if (!worktreeProfile) {
1367
1758
  server.tool(
1368
1759
  "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.",
1760
+ "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
1761
  {},
1371
1762
  async () => {
1372
1763
  const workspaces = orch.listWorkspaces();
@@ -1400,6 +1791,51 @@ async function startMcpServer() {
1400
1791
  };
1401
1792
  }
1402
1793
  );
1794
+ server.tool(
1795
+ "list_board",
1796
+ "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.",
1797
+ {
1798
+ query: z4.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
1799
+ repoPath: z4.string().optional().describe("Limit to one workspace path from list_workspaces"),
1800
+ kind: z4.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
1801
+ column: z4.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
1802
+ "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
1803
+ ),
1804
+ limit: z4.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
1805
+ },
1806
+ async ({ query, repoPath, kind, column, limit }) => {
1807
+ const workspaces = orch.listWorkspaces();
1808
+ const all = orch.getThreads(true);
1809
+ const names = new Map(workspaces.map((w) => [w.path, w.name]));
1810
+ const snap = assembleHomeBoard({
1811
+ threads: all.filter((t) => t.status !== "archived"),
1812
+ query,
1813
+ repoPath,
1814
+ kind: kind ?? "all",
1815
+ column: column === "needs_you" ? "new" : column,
1816
+ limit,
1817
+ workspaceName: (path) => names.get(path) ?? ""
1818
+ });
1819
+ return {
1820
+ content: [
1821
+ {
1822
+ type: "text",
1823
+ text: JSON.stringify(
1824
+ {
1825
+ columns: snap.columns,
1826
+ hidden: snap.hidden,
1827
+ totals: snap.totals,
1828
+ columnDefs: BOARD_COLUMN_DEFS,
1829
+ hint: HOME_BOARD_AGENT_HINT
1830
+ },
1831
+ null,
1832
+ 2
1833
+ )
1834
+ }
1835
+ ]
1836
+ };
1837
+ }
1838
+ );
1403
1839
  server.tool(
1404
1840
  "get_thread",
1405
1841
  "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.",
@@ -1438,25 +1874,31 @@ async function startMcpServer() {
1438
1874
  }
1439
1875
  server.tool(
1440
1876
  "present_artifact",
1441
- "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.",
1877
+ "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.",
1442
1878
  {
1443
1879
  title: z4.string().describe("Short title shown in the artifact pane header"),
1444
- type: z4.enum(["html", "svg", "markdown", "react"]).describe(
1445
- "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
1880
+ type: z4.enum(["html", "svg", "markdown", "react", "log"]).describe(
1881
+ "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
1446
1882
  ),
1447
1883
  content: z4.string().describe(
1448
- "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
1884
+ "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
1449
1885
  ),
1450
- artifact_id: z4.string().optional().describe("Stable id when updating the same artifact across turns")
1886
+ artifact_id: z4.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
1887
+ status: z4.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
1888
+ phase: z4.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
1889
+ mode: z4.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
1451
1890
  },
1452
- async ({ title, type, artifact_id }) => {
1891
+ async ({ title, type, artifact_id, status, phase, mode }) => {
1453
1892
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1454
1893
  const payload = {
1455
1894
  ok: true,
1456
1895
  artifact_id: id,
1457
1896
  title,
1458
1897
  type,
1459
- message: "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
1898
+ status,
1899
+ phase,
1900
+ mode: type === "log" ? mode ?? "append" : void 0,
1901
+ 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."
1460
1902
  };
1461
1903
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
1462
1904
  }
@@ -1569,7 +2011,7 @@ async function startMcpServer() {
1569
2011
  registerLinearTools(server);
1570
2012
  registerScheduleTools(server);
1571
2013
  const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-NBRUWYXO.js");
1572
- const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-IKFWZDUK.js");
2014
+ const { resolveNewThreadOptions, resolveThreadDefaults } = await import("../app-settings-E7NEQY7D.js");
1573
2015
  const accountDefaults = resolveThreadDefaults();
1574
2016
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
1575
2017
  server.tool(
@@ -1612,7 +2054,7 @@ async function startMcpServer() {
1612
2054
  );
1613
2055
  server.tool(
1614
2056
  "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.`,
2057
+ `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
2058
  {
1617
2059
  sourceType: z4.enum(["branch", "pr", "ticket"]),
1618
2060
  sourceRef: z4.string(),
@@ -1630,104 +2072,118 @@ async function startMcpServer() {
1630
2072
  )
1631
2073
  },
1632
2074
  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;
2075
+ const result = await createOrchChildThread(orch, args, resolveNewThreadOptions);
2076
+ return {
2077
+ content: [{ type: "text", text: result.text }],
2078
+ ...result.ok ? {} : { isError: true }
2079
+ };
2080
+ }
2081
+ );
2082
+ server.tool(
2083
+ "start_board_card",
2084
+ "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.",
2085
+ {
2086
+ kind: z4.enum(["ticket", "pr", "branch"]),
2087
+ ref: z4.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
2088
+ repoPath: z4.string().describe("Workspace path from list_workspaces / list_board"),
2089
+ title: z4.string().optional()
2090
+ },
2091
+ async ({ kind, ref, repoPath, title }) => {
2092
+ const root = await resolveRepoRoot(repoPath);
2093
+ const existing = findLiveThreadForCreate(
2094
+ {
2095
+ sourceType: kind,
2096
+ sourceRef: ref.trim(),
2097
+ repoPath: canonicalizeRepoPath(root),
2098
+ title: title?.trim()
2099
+ },
2100
+ orch.getThreads(false).map((t) => ({
2101
+ ...t,
2102
+ repoPath: canonicalizeRepoPath(t.repoPath)
2103
+ }))
2104
+ );
2105
+ if (existing) {
1695
2106
  return {
1696
2107
  content: [
1697
2108
  {
1698
2109
  type: "text",
1699
2110
  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 } : {}
2111
+ alreadyStarted: true,
2112
+ id: existing.id,
2113
+ title: existing.title,
2114
+ status: existing.status,
2115
+ link: `sideboard://thread/${existing.id}`
1715
2116
  })
1716
2117
  }
1717
2118
  ]
1718
2119
  };
1719
- } catch (err) {
1720
- const message = err instanceof Error ? err.message : String(err);
2120
+ }
2121
+ const workspaces = orch.listWorkspaces();
2122
+ const loaded = await getHomeBoardInputs(workspaces);
2123
+ const pin = findBoardPin(loaded.pins, kind, ref, root);
2124
+ if (kind === "branch") {
2125
+ const sourceRef = pin?.ref === "default" ? "default" : pin?.ref ?? ref.trim();
2126
+ const result2 = await createOrchChildThread(
2127
+ orch,
2128
+ {
2129
+ sourceType: "branch",
2130
+ sourceRef,
2131
+ repoPath: root,
2132
+ title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
2133
+ },
2134
+ resolveNewThreadOptions
2135
+ );
1721
2136
  return {
1722
- content: [
1723
- {
1724
- type: "text",
1725
- text: `create_thread failed: ${message}`
1726
- }
1727
- ],
1728
- isError: true
2137
+ content: [{ type: "text", text: result2.text }],
2138
+ ...result2.ok ? {} : { isError: true }
2139
+ };
2140
+ }
2141
+ if (kind === "ticket") {
2142
+ const issue = findBoardIssue(loaded.issues, ref, root);
2143
+ const ident = issue?.identifier ?? ref.trim();
2144
+ const attachments = issue ? [
2145
+ {
2146
+ id: randomUUID2(),
2147
+ name: issue.identifier,
2148
+ kind: "issue",
2149
+ content: [
2150
+ `Linked issue: ${issue.identifier} \u2014 ${issue.title}`,
2151
+ issue.url ? `URL: ${issue.url}` : null
2152
+ ].filter(Boolean).join("\n")
2153
+ }
2154
+ ] : void 0;
2155
+ const result2 = await createOrchChildThread(
2156
+ orch,
2157
+ {
2158
+ sourceType: "ticket",
2159
+ sourceRef: ident,
2160
+ repoPath: root,
2161
+ title: title?.trim() || issue?.title,
2162
+ attachments
2163
+ },
2164
+ resolveNewThreadOptions
2165
+ );
2166
+ return {
2167
+ content: [{ type: "text", text: result2.text }],
2168
+ ...result2.ok ? {} : { isError: true }
1729
2169
  };
1730
2170
  }
2171
+ const pr = findBoardPr(loaded.prs, ref, root);
2172
+ const number = pr ? String(pr.number) : ref.trim().replace(/^#/, "");
2173
+ const result = await createOrchChildThread(
2174
+ orch,
2175
+ {
2176
+ sourceType: "pr",
2177
+ sourceRef: number,
2178
+ repoPath: root,
2179
+ title: title?.trim() || pr?.title
2180
+ },
2181
+ resolveNewThreadOptions
2182
+ );
2183
+ return {
2184
+ content: [{ type: "text", text: result.text }],
2185
+ ...result.ok ? {} : { isError: true }
2186
+ };
1731
2187
  }
1732
2188
  );
1733
2189
  server.tool(
@@ -2374,7 +2830,7 @@ async function startMcpServer() {
2374
2830
  );
2375
2831
  server.tool(
2376
2832
  "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.",
2833
+ "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
2834
  { repoPath: z4.string() },
2379
2835
  async ({ repoPath }) => {
2380
2836
  const root = await resolveRepoRoot(repoPath);