@sideboard-ai/core 0.1.156 → 0.1.157

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 (27) hide show
  1. package/dist/{abletime-JSTJLXAW.js → abletime-BEIDJG7W.js} +5 -1
  2. package/dist/{abletime-H7LPGIP2.js → abletime-GWZE6OYM.js} +5 -1
  3. package/dist/{agents-2FTZAXO3.js → agents-C55LAEUR.js} +2 -2
  4. package/dist/{agents-IY4CSPWV.js → agents-LCDXN2B2.js} +2 -2
  5. package/dist/{chunk-TI6UMRWK.js → chunk-4I4VKAPZ.js} +3 -2
  6. package/dist/{chunk-MSFPM5VK.js → chunk-CVR4RJ6G.js} +3 -2
  7. package/dist/{chunk-JOK4XWBG.js → chunk-DVJBO3M7.js} +87 -3
  8. package/dist/{chunk-7EIPEPAH.js → chunk-GWEEGE6L.js} +119 -10
  9. package/dist/{chunk-UA3YZKRX.js → chunk-KQ4JOGUH.js} +19 -3
  10. package/dist/{chunk-FDCFXMDG.js → chunk-N27GVFZY.js} +87 -3
  11. package/dist/{chunk-OLE2BLIX.js → chunk-SUCJAWV4.js} +90 -9
  12. package/dist/{chunk-XLU7DXHH.js → chunk-TIJ6QVX5.js} +47 -3
  13. package/dist/{coordinator-prompt-D2FHU5IZ.js → coordinator-prompt-26IJU2AV.js} +1 -1
  14. package/dist/{coordinator-prompt-UZDNIVP7.js → coordinator-prompt-AM47SATF.js} +1 -1
  15. package/dist/{global-workspace-5WZVSWNT.js → global-workspace-5BIW26YR.js} +1 -1
  16. package/dist/{global-workspace-MR75BDBY.js → global-workspace-QV7CC7SK.js} +1 -1
  17. package/dist/index.cjs +951 -379
  18. package/dist/index.d.cts +128 -3
  19. package/dist/index.d.ts +128 -3
  20. package/dist/index.js +590 -252
  21. package/dist/mcp/run-stdio.cjs +799 -320
  22. package/dist/mcp/run-stdio.js +501 -199
  23. package/dist/{orchestrator-G4WKJG5Y.js → orchestrator-BUH3LJLL.js} +3 -3
  24. package/dist/{orchestrator-LXO2CDOG.js → orchestrator-KQO4MXBI.js} +3 -3
  25. package/dist/{workspaces-RP32AW6X.js → workspaces-NYSM2EPX.js} +1 -1
  26. package/dist/{workspaces-HO4EQNNM.js → workspaces-Q2VCHIAS.js} +1 -1
  27. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -97,6 +97,10 @@ import {
97
97
  formatAgentInstructions,
98
98
  formatArtifactDirective,
99
99
  formatDetachedJobInvoke,
100
+ formatIssueToolsDirective,
101
+ formatIssueToolsReminder,
102
+ formatLinearDirective,
103
+ formatLinearReminder,
100
104
  formatLongRunningDirective,
101
105
  formatLongRunningReminder,
102
106
  formatMessagesAsTranscript,
@@ -144,7 +148,9 @@ import {
144
148
  isStaleLastErrorDuringTurn,
145
149
  isThisProcessDesktopHost,
146
150
  issueNeedsWorkspacePick,
151
+ issueTicketFromThread,
147
152
  lastRequestOccupancy,
153
+ linearTicketIdFromThread,
148
154
  listBranchCommits,
149
155
  listConductorWorkspaces,
150
156
  listLinearIssues,
@@ -223,7 +229,7 @@ import {
223
229
  worktreeCleanupSettings,
224
230
  wrapReviewSkillMarkdown,
225
231
  writeWorktreeFile
226
- } from "./chunk-7EIPEPAH.js";
232
+ } from "./chunk-GWEEGE6L.js";
227
233
  import {
228
234
  LEGACY_PLAN_FILE_REL,
229
235
  PLAN_FILE_NAME,
@@ -242,6 +248,9 @@ import {
242
248
  PLAN_MODE_INSTRUCTION,
243
249
  SIDEBOARD_MCP_ALLOWED_TOOLS,
244
250
  SIDEBOARD_MCP_PROFILE_ENV,
251
+ WORKTREE_ABLETIME_MCP_TOOLS,
252
+ WORKTREE_GITHUB_MCP_TOOLS,
253
+ WORKTREE_LINEAR_MCP_TOOLS,
245
254
  WORKTREE_MCP_TOOLS,
246
255
  allAdapters,
247
256
  applyAgentEvent,
@@ -323,7 +332,7 @@ import {
323
332
  withEventParentId,
324
333
  withEventsParentId,
325
334
  writeInjectedMcpConfig
326
- } from "./chunk-XLU7DXHH.js";
335
+ } from "./chunk-TIJ6QVX5.js";
327
336
  import {
328
337
  CLOUD_COORDINATOR_BUSY_REPLY,
329
338
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -370,7 +379,7 @@ import {
370
379
  stageBuffersAsAttachments,
371
380
  syncWorkspacesFromThreads,
372
381
  takenTeamSlugsForOrchestration
373
- } from "./chunk-MSFPM5VK.js";
382
+ } from "./chunk-CVR4RJ6G.js";
374
383
  import {
375
384
  FAMOUS_SOCCER_TEAMS,
376
385
  GITHUB_PR_BODY_MAX_CHARS,
@@ -496,6 +505,7 @@ import {
496
505
  abletimeMcpRequest,
497
506
  abletimeMcpUrl,
498
507
  callAbleTimeTool,
508
+ commentAbleTimeTask,
499
509
  createAbleTimeTask,
500
510
  ensureAbleTimeTask,
501
511
  getAbleTimeOrientation,
@@ -510,8 +520,9 @@ import {
510
520
  searchAbleTimeTasks,
511
521
  taskUrl,
512
522
  toAbleTimeIssueInfo,
523
+ updateAbleTimeTask,
513
524
  verifyAbleTimeConnection
514
- } from "./chunk-JOK4XWBG.js";
525
+ } from "./chunk-DVJBO3M7.js";
515
526
  import {
516
527
  formatFetchError,
517
528
  httpFetch,
@@ -722,9 +733,9 @@ async function getGitHubStatus() {
722
733
  };
723
734
  }
724
735
  const status = await run("gh", ["auth", "status"], { reject: false });
725
- const text5 = `${status.stdout}
736
+ const text6 = `${status.stdout}
726
737
  ${status.stderr}`;
727
- const loginMatch = text5.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text5.match(/Logged in to ([^\s]+) as (\S+)/i);
738
+ const loginMatch = text6.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text6.match(/Logged in to ([^\s]+) as (\S+)/i);
728
739
  if (loginMatch) {
729
740
  return {
730
741
  connected: true,
@@ -1504,6 +1515,11 @@ async function createLinearIssue(input, opts) {
1504
1515
  if (input.state?.trim()) {
1505
1516
  mutationInput.stateId = resolveLinearState(team, input.state).id;
1506
1517
  }
1518
+ const parentRef = input.parent?.trim();
1519
+ if (parentRef) {
1520
+ const parent = await getLinearIssue(parentRef, opts);
1521
+ mutationInput.parentId = parent.id;
1522
+ }
1507
1523
  const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
1508
1524
  if (assignee === "me") mutationInput.assigneeId = viewer.id;
1509
1525
  else if (assignee) mutationInput.assigneeId = assignee;
@@ -1584,6 +1600,190 @@ async function validateLinearApiKey(apiKey) {
1584
1600
  }
1585
1601
  }
1586
1602
 
1603
+ // src/integrations/github-issues.ts
1604
+ function requireGhOk(result, label) {
1605
+ if (result.exitCode !== 0) {
1606
+ const detail = (result.stderr || result.stdout).trim() || "gh failed";
1607
+ throw new Error(`${label}: ${detail}`);
1608
+ }
1609
+ return result.stdout;
1610
+ }
1611
+ function parseGitHubIssueNumber(id) {
1612
+ const trimmed = id.trim();
1613
+ if (!trimmed) throw new Error("GitHub issue id is required (#123 or a URL)");
1614
+ const url = trimmed.match(/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/i);
1615
+ if (url) return Number(url[1]);
1616
+ const prefixed = trimmed.match(/^gh-(\d+)$/i);
1617
+ if (prefixed) return Number(prefixed[1]);
1618
+ const bare = trimmed.match(/^#?(\d+)$/);
1619
+ if (bare) return Number(bare[1]);
1620
+ throw new Error(`GitHub issue id must be #123, a number, or an issue URL (got ${trimmed})`);
1621
+ }
1622
+ async function resolveGitHubIssueRepo(repoPath) {
1623
+ const cwd = await resolveRepoRoot((repoPath ?? "").trim() || process.cwd());
1624
+ const slug = await resolveGithubRepoSlug(cwd);
1625
+ return { cwd, slug, repoArgs: slug ? ghRepoSelectArgs(slug) : [] };
1626
+ }
1627
+ function mapLabels(raw) {
1628
+ if (!Array.isArray(raw)) return [];
1629
+ return raw.map((item) => typeof item === "string" ? item : String(item?.name ?? "")).map((name) => name.trim()).filter(Boolean);
1630
+ }
1631
+ function mapAssignees(raw) {
1632
+ if (!Array.isArray(raw)) return [];
1633
+ return raw.map(
1634
+ (item) => typeof item === "string" ? item : String(item?.login ?? "")
1635
+ ).map((login) => login.trim()).filter(Boolean);
1636
+ }
1637
+ function mapComments2(raw) {
1638
+ if (!Array.isArray(raw)) return [];
1639
+ return raw.map((item) => {
1640
+ const rec = item && typeof item === "object" ? item : null;
1641
+ if (!rec) return null;
1642
+ const body = typeof rec.body === "string" ? rec.body : "";
1643
+ const authorRec = rec.author && typeof rec.author === "object" ? rec.author : null;
1644
+ const comment = {
1645
+ body,
1646
+ id: rec.id != null ? String(rec.id) : void 0,
1647
+ url: typeof rec.url === "string" ? rec.url : void 0,
1648
+ createdAt: typeof rec.createdAt === "string" ? rec.createdAt : void 0,
1649
+ author: authorRec?.login?.trim() || void 0
1650
+ };
1651
+ return comment;
1652
+ }).filter((item) => Boolean(item));
1653
+ }
1654
+ function toGitHubIssue(raw) {
1655
+ const number = Number(raw.number);
1656
+ if (!Number.isFinite(number) || number <= 0) {
1657
+ throw new Error("GitHub issue response was missing a number");
1658
+ }
1659
+ const assignees = mapAssignees(raw.assignees);
1660
+ return {
1661
+ id: `gh-${number}`,
1662
+ identifier: `#${number}`,
1663
+ number,
1664
+ title: String(raw.title ?? ""),
1665
+ url: String(raw.url ?? ""),
1666
+ body: typeof raw.body === "string" && raw.body.trim() ? raw.body : void 0,
1667
+ state: typeof raw.state === "string" ? raw.state : void 0,
1668
+ labels: mapLabels(raw.labels),
1669
+ assignees,
1670
+ comments: mapComments2(raw.comments)
1671
+ };
1672
+ }
1673
+ function toGitHubIssueInfo(issue) {
1674
+ return {
1675
+ id: issue.id,
1676
+ identifier: issue.identifier,
1677
+ title: issue.title,
1678
+ url: issue.url,
1679
+ labels: issue.labels,
1680
+ provider: "github",
1681
+ assignee: issue.assignees[0],
1682
+ assignees: issue.assignees.length ? issue.assignees : void 0
1683
+ };
1684
+ }
1685
+ async function getGitHubIssue(id, opts) {
1686
+ const number = parseGitHubIssueNumber(id);
1687
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
1688
+ const stdout = requireGhOk(
1689
+ await gh(
1690
+ [
1691
+ "issue",
1692
+ "view",
1693
+ String(number),
1694
+ ...repoArgs,
1695
+ "--json",
1696
+ "number,title,body,url,state,labels,assignees,comments,author"
1697
+ ],
1698
+ cwd,
1699
+ { reject: false }
1700
+ ),
1701
+ `GitHub issue ${number}`
1702
+ );
1703
+ let parsed;
1704
+ try {
1705
+ parsed = JSON.parse(stdout);
1706
+ } catch {
1707
+ throw new Error(`GitHub issue ${number}: gh returned non-JSON`);
1708
+ }
1709
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1710
+ throw new Error(`GitHub issue not found: #${number}`);
1711
+ }
1712
+ return toGitHubIssue(parsed);
1713
+ }
1714
+ async function commentGitHubIssue(input, opts) {
1715
+ const number = parseGitHubIssueNumber(input.id);
1716
+ const body = input.body.trim();
1717
+ if (!body) throw new Error("GitHub comment body is required");
1718
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
1719
+ const stdout = requireGhOk(
1720
+ await gh(
1721
+ ["issue", "comment", String(number), ...repoArgs, "--body", body],
1722
+ cwd,
1723
+ { reject: false }
1724
+ ),
1725
+ `GitHub comment on #${number}`
1726
+ );
1727
+ const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
1728
+ return { body, url };
1729
+ }
1730
+ async function updateGitHubIssue(input, opts) {
1731
+ const number = parseGitHubIssueNumber(input.id);
1732
+ const title = input.title?.trim();
1733
+ const body = input.body;
1734
+ const state = input.state?.trim().toLowerCase();
1735
+ if (!title && body === void 0 && !state) {
1736
+ throw new Error("github_update_issue needs at least one of title, body, state");
1737
+ }
1738
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
1739
+ if (state === "closed" || state === "close") {
1740
+ requireGhOk(
1741
+ await gh(["issue", "close", String(number), ...repoArgs], cwd, { reject: false }),
1742
+ `GitHub close #${number}`
1743
+ );
1744
+ } else if (state === "open" || state === "reopen") {
1745
+ requireGhOk(
1746
+ await gh(["issue", "reopen", String(number), ...repoArgs], cwd, { reject: false }),
1747
+ `GitHub reopen #${number}`
1748
+ );
1749
+ } else if (state) {
1750
+ throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
1751
+ }
1752
+ if (title || body !== void 0) {
1753
+ const args = ["issue", "edit", String(number), ...repoArgs];
1754
+ if (title) args.push("--title", title);
1755
+ if (body !== void 0) args.push("--body", body);
1756
+ requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
1757
+ }
1758
+ return getGitHubIssue(String(number), opts);
1759
+ }
1760
+ async function createGitHubIssue(input, opts) {
1761
+ const title = input.title.trim();
1762
+ if (!title) throw new Error("GitHub issue title is required");
1763
+ const parent = input.parent?.trim();
1764
+ const parentNumber = parent ? parseGitHubIssueNumber(parent) : null;
1765
+ const bodyParts = [
1766
+ parentNumber ? `Spin-off of #${parentNumber}.` : null,
1767
+ input.body?.trim() || null
1768
+ ].filter(Boolean);
1769
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
1770
+ const args = ["issue", "create", ...repoArgs, "--title", title];
1771
+ if (bodyParts.length) args.push("--body", bodyParts.join("\n\n"));
1772
+ const created = await gh([...args, "--json", "number,url,title"], cwd, { reject: false });
1773
+ if (created.exitCode === 0 && created.stdout.trim()) {
1774
+ try {
1775
+ const parsed = JSON.parse(created.stdout);
1776
+ if (parsed.number) return getGitHubIssue(String(parsed.number), opts);
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ const fallback = created.exitCode === 0 ? created : await gh(args, cwd, { reject: false });
1781
+ const stdout = requireGhOk(fallback, "GitHub create issue");
1782
+ const url = stdout.trim().match(/https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/i);
1783
+ if (url?.[1]) return getGitHubIssue(url[1], opts);
1784
+ throw new Error(`GitHub create issue: could not parse issue from ${stdout.trim() || "empty output"}`);
1785
+ }
1786
+
1587
1787
  // src/integrations/issues.ts
1588
1788
  function assigneeKey(assignee) {
1589
1789
  return (assignee ?? "").trim().toLowerCase();
@@ -1860,11 +2060,11 @@ function fenceLanguage(path2, language) {
1860
2060
  }
1861
2061
  function buildCodeRefAttachment(input) {
1862
2062
  const path2 = input.path.trim();
1863
- const text5 = input.text.replace(/\n$/, "");
2063
+ const text6 = input.text.replace(/\n$/, "");
1864
2064
  if (!path2) {
1865
2065
  throw new Error("code reference requires a file path");
1866
2066
  }
1867
- if (!text5.trim()) {
2067
+ if (!text6.trim()) {
1868
2068
  throw new Error("code reference requires selected text");
1869
2069
  }
1870
2070
  if (input.startLine < 1 || input.endLine < 1 || input.endLine < input.startLine) {
@@ -1877,7 +2077,7 @@ function buildCodeRefAttachment(input) {
1877
2077
  `Referenced code from \`${path2}\` (${range}).`,
1878
2078
  "",
1879
2079
  fence,
1880
- text5,
2080
+ text6,
1881
2081
  "```"
1882
2082
  ].join("\n");
1883
2083
  return {
@@ -1931,16 +2131,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
1931
2131
  var PASTE_ATTACH_MIN_LINES = 15;
1932
2132
  var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
1933
2133
  var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
1934
- function pastedTextStats(text5) {
1935
- const chars = text5.length;
2134
+ function pastedTextStats(text6) {
2135
+ const chars = text6.length;
1936
2136
  if (chars === 0) return { chars: 0, lines: 0 };
1937
- const lines = text5.split(/\r\n|\r|\n/).length;
2137
+ const lines = text6.split(/\r\n|\r|\n/).length;
1938
2138
  return { chars, lines };
1939
2139
  }
1940
- function shouldAttachPastedText(text5) {
1941
- const trimmed = text5.trim();
2140
+ function shouldAttachPastedText(text6) {
2141
+ const trimmed = text6.trim();
1942
2142
  if (!trimmed) return false;
1943
- const { chars, lines } = pastedTextStats(text5);
2143
+ const { chars, lines } = pastedTextStats(text6);
1944
2144
  return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
1945
2145
  }
1946
2146
  function nextPastedTextName(existing) {
@@ -1951,13 +2151,13 @@ function nextPastedTextName(existing) {
1951
2151
  }
1952
2152
  return `Pasted text #${max + 1}.txt`;
1953
2153
  }
1954
- function buildPastedTextAttachment(text5, opts) {
2154
+ function buildPastedTextAttachment(text6, opts) {
1955
2155
  return {
1956
2156
  id: opts?.id ?? randomUUID(),
1957
2157
  name: opts?.name ?? "Pasted text #1.txt",
1958
2158
  kind: "file",
1959
2159
  path: opts?.path,
1960
- content: text5
2160
+ content: text6
1961
2161
  };
1962
2162
  }
1963
2163
 
@@ -2042,8 +2242,8 @@ function latestPendingPlanQuestions(input) {
2042
2242
  return null;
2043
2243
  }
2044
2244
  var PLAN_QUESTION_ANSWERS_PREFIX = "Answers to your questions:";
2045
- function isPlanQuestionAnswersMessage(text5) {
2046
- return text5.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
2245
+ function isPlanQuestionAnswersMessage(text6) {
2246
+ return text6.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
2047
2247
  }
2048
2248
  function formatPlanQuestionAnswers(questions, answers) {
2049
2249
  const lines = [PLAN_QUESTION_ANSWERS_PREFIX, ""];
@@ -2082,7 +2282,7 @@ function formatPlanQuestionsForChat(questions) {
2082
2282
  // src/mcp/server.ts
2083
2283
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2084
2284
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2085
- import { z as z5 } from "zod";
2285
+ import { z as z6 } from "zod";
2086
2286
  import { randomUUID as randomUUID3 } from "crypto";
2087
2287
  import { basename } from "path";
2088
2288
 
@@ -2120,9 +2320,9 @@ function mcpWaitFinishedHint(status) {
2120
2320
  function lastMessagePreview(messages, max = 160) {
2121
2321
  if (!messages?.length) return null;
2122
2322
  for (let i = messages.length - 1; i >= 0; i--) {
2123
- const text5 = messages[i]?.text?.trim();
2124
- if (!text5 || isInternalAgentStatusText(text5)) continue;
2125
- const flat = text5.replace(/\s+/g, " ");
2323
+ const text6 = messages[i]?.text?.trim();
2324
+ if (!text6 || isInternalAgentStatusText(text6)) continue;
2325
+ const flat = text6.replace(/\s+/g, " ");
2126
2326
  return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
2127
2327
  }
2128
2328
  return null;
@@ -2354,8 +2554,8 @@ function labelGithubUrl(url) {
2354
2554
  }
2355
2555
  return { kind: "other", label: "GitHub", url: raw };
2356
2556
  }
2357
- function appendGithubLink(text5, githubUrl) {
2358
- const body = text5.trimEnd();
2557
+ function appendGithubLink(text6, githubUrl) {
2558
+ const body = text6.trimEnd();
2359
2559
  if (!githubUrl?.trim()) return body;
2360
2560
  const labeled = labelGithubUrl(githubUrl);
2361
2561
  if (!labeled) {
@@ -2751,11 +2951,51 @@ function registerAbleTimeTools(server) {
2751
2951
  );
2752
2952
  server.tool(
2753
2953
  "abletime_get_task",
2754
- "Get one AbleTime task by id or reference (e.g. CRM-232).",
2954
+ "Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments. Re-fetch to read new comments.",
2755
2955
  { id: z2.string() },
2756
2956
  async ({ id }) => {
2757
2957
  try {
2758
- return text2(toAbleTimeIssueInfo(await getAbleTimeTask(id)));
2958
+ const task = await getAbleTimeTask(id);
2959
+ return text2({
2960
+ ...toAbleTimeIssueInfo(task),
2961
+ description: task.description,
2962
+ state: task.state,
2963
+ comments: task.comments
2964
+ });
2965
+ } catch (err) {
2966
+ return fail2(err);
2967
+ }
2968
+ }
2969
+ );
2970
+ server.tool(
2971
+ "abletime_comment",
2972
+ "Add a markdown comment on an AbleTime task (id or CRM-232).",
2973
+ { id: z2.string(), body: z2.string() },
2974
+ async (args) => {
2975
+ try {
2976
+ return text2(await commentAbleTimeTask(args));
2977
+ } catch (err) {
2978
+ return fail2(err);
2979
+ }
2980
+ }
2981
+ );
2982
+ server.tool(
2983
+ "abletime_update_task",
2984
+ "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state.",
2985
+ {
2986
+ id: z2.string(),
2987
+ title: z2.string().optional(),
2988
+ description: z2.string().optional(),
2989
+ state: z2.string().optional()
2990
+ },
2991
+ async (args) => {
2992
+ try {
2993
+ const task = await updateAbleTimeTask(args);
2994
+ return text2({
2995
+ ...toAbleTimeIssueInfo(task),
2996
+ description: task.description,
2997
+ state: task.state
2998
+ });
2759
2999
  } catch (err) {
2760
3000
  return fail2(err);
2761
3001
  }
@@ -2763,13 +3003,14 @@ function registerAbleTimeTools(server) {
2763
3003
  );
2764
3004
  server.tool(
2765
3005
  "abletime_create_task",
2766
- "Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. New tasks start in todo (or backlog).",
3006
+ "Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. Pass parent (CRM-232) for a spin-off. New tasks start in todo (or backlog).",
2767
3007
  {
2768
3008
  title: z2.string(),
2769
3009
  description: z2.string().optional(),
2770
3010
  projectId: z2.string().optional(),
2771
3011
  categoryId: z2.string().optional(),
2772
- state: z2.enum(["backlog", "todo"]).optional()
3012
+ state: z2.enum(["backlog", "todo"]).optional(),
3013
+ parent: z2.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
2773
3014
  },
2774
3015
  async (args) => {
2775
3016
  try {
@@ -2799,7 +3040,7 @@ function registerAbleTimeTools(server) {
2799
3040
  );
2800
3041
  }
2801
3042
 
2802
- // src/mcp/linear-tools.ts
3043
+ // src/mcp/github-tools.ts
2803
3044
  import { z as z3 } from "zod";
2804
3045
  function text3(payload, isError = false) {
2805
3046
  return mcpJson(payload, isError);
@@ -2810,7 +3051,81 @@ function fail3(err) {
2810
3051
  true
2811
3052
  );
2812
3053
  }
2813
- var prioritySchema = z3.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
3054
+ var repoPathSchema = z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
3055
+ function registerGithubIssueTools(server) {
3056
+ server.tool(
3057
+ "github_get_issue",
3058
+ "Get a GitHub issue (#123 or URL): body, comments, state. Re-fetch to read new comments. Uses Account gh.",
3059
+ { id: z3.string(), repoPath: repoPathSchema },
3060
+ async ({ id, repoPath }) => {
3061
+ try {
3062
+ return text3(await getGitHubIssue(id, { repoPath }));
3063
+ } catch (err) {
3064
+ return fail3(err);
3065
+ }
3066
+ }
3067
+ );
3068
+ server.tool(
3069
+ "github_comment",
3070
+ "Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh.",
3071
+ { id: z3.string(), body: z3.string(), repoPath: repoPathSchema },
3072
+ async ({ id, body, repoPath }) => {
3073
+ try {
3074
+ return text3(await commentGitHubIssue({ id, body }, { repoPath }));
3075
+ } catch (err) {
3076
+ return fail3(err);
3077
+ }
3078
+ }
3079
+ );
3080
+ server.tool(
3081
+ "github_update_issue",
3082
+ "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed).",
3083
+ {
3084
+ id: z3.string(),
3085
+ title: z3.string().optional(),
3086
+ body: z3.string().optional(),
3087
+ state: z3.string().optional().describe("open or closed"),
3088
+ repoPath: repoPathSchema
3089
+ },
3090
+ async (args) => {
3091
+ try {
3092
+ return text3(await updateGitHubIssue(args, { repoPath: args.repoPath }));
3093
+ } catch (err) {
3094
+ return fail3(err);
3095
+ }
3096
+ }
3097
+ );
3098
+ server.tool(
3099
+ "github_create_issue",
3100
+ "Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
3101
+ {
3102
+ title: z3.string(),
3103
+ body: z3.string().optional(),
3104
+ parent: z3.string().optional().describe("Parent issue #123 or URL for a spin-off."),
3105
+ repoPath: repoPathSchema
3106
+ },
3107
+ async (args) => {
3108
+ try {
3109
+ return text3(await createGitHubIssue(args, { repoPath: args.repoPath }));
3110
+ } catch (err) {
3111
+ return fail3(err);
3112
+ }
3113
+ }
3114
+ );
3115
+ }
3116
+
3117
+ // src/mcp/linear-tools.ts
3118
+ import { z as z4 } from "zod";
3119
+ function text4(payload, isError = false) {
3120
+ return mcpJson(payload, isError);
3121
+ }
3122
+ function fail4(err) {
3123
+ return text4(
3124
+ { error: err instanceof Error ? err.message : String(err) },
3125
+ true
3126
+ );
3127
+ }
3128
+ var prioritySchema = z4.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
2814
3129
  function registerLinearTools(server) {
2815
3130
  server.tool(
2816
3131
  "linear_list_teams",
@@ -2818,9 +3133,9 @@ function registerLinearTools(server) {
2818
3133
  {},
2819
3134
  async () => {
2820
3135
  try {
2821
- return text3(await listLinearTeams());
3136
+ return text4(await listLinearTeams());
2822
3137
  } catch (err) {
2823
- return fail3(err);
3138
+ return fail4(err);
2824
3139
  }
2825
3140
  }
2826
3141
  );
@@ -2828,9 +3143,9 @@ function registerLinearTools(server) {
2828
3143
  "linear_search_issues",
2829
3144
  "Search or list open Linear issues. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all (default with query), or a user id/name.",
2830
3145
  {
2831
- query: z3.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
2832
- assignee: z3.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
2833
- limit: z3.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
3146
+ query: z4.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
3147
+ assignee: z4.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
3148
+ limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
2834
3149
  },
2835
3150
  async ({ query, assignee, limit }) => {
2836
3151
  try {
@@ -2851,38 +3166,39 @@ function registerLinearTools(server) {
2851
3166
  })
2852
3167
  );
2853
3168
  } catch (err) {
2854
- return fail3(err);
3169
+ return fail4(err);
2855
3170
  }
2856
3171
  }
2857
3172
  );
2858
3173
  server.tool(
2859
3174
  "linear_get_issue",
2860
3175
  "Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children.",
2861
- { id: z3.string() },
3176
+ { id: z4.string() },
2862
3177
  async ({ id }) => {
2863
3178
  try {
2864
- return text3(await getLinearIssue(id));
3179
+ return text4(await getLinearIssue(id));
2865
3180
  } catch (err) {
2866
- return fail3(err);
3181
+ return fail4(err);
2867
3182
  }
2868
3183
  }
2869
3184
  );
2870
3185
  server.tool(
2871
3186
  "linear_create_issue",
2872
- 'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id.',
3187
+ 'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id. Pass parent (ENG-123 or uuid) to nest a spin-off under the current ticket.',
2873
3188
  {
2874
- team: z3.string(),
2875
- title: z3.string(),
2876
- description: z3.string().optional(),
2877
- state: z3.string().optional(),
2878
- assignee: z3.string().nullable().optional(),
2879
- priority: prioritySchema
3189
+ team: z4.string(),
3190
+ title: z4.string(),
3191
+ description: z4.string().optional(),
3192
+ state: z4.string().optional(),
3193
+ assignee: z4.string().nullable().optional(),
3194
+ priority: prioritySchema,
3195
+ parent: z4.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
2880
3196
  },
2881
3197
  async (args) => {
2882
3198
  try {
2883
- return text3(await createLinearIssue(args));
3199
+ return text4(await createLinearIssue(args));
2884
3200
  } catch (err) {
2885
- return fail3(err);
3201
+ return fail4(err);
2886
3202
  }
2887
3203
  }
2888
3204
  );
@@ -2890,18 +3206,18 @@ function registerLinearTools(server) {
2890
3206
  "linear_update_issue",
2891
3207
  "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority.",
2892
3208
  {
2893
- id: z3.string(),
2894
- title: z3.string().optional(),
2895
- description: z3.string().optional(),
2896
- state: z3.string().optional(),
2897
- assignee: z3.string().nullable().optional(),
3209
+ id: z4.string(),
3210
+ title: z4.string().optional(),
3211
+ description: z4.string().optional(),
3212
+ state: z4.string().optional(),
3213
+ assignee: z4.string().nullable().optional(),
2898
3214
  priority: prioritySchema
2899
3215
  },
2900
3216
  async (args) => {
2901
3217
  try {
2902
- return text3(await updateLinearIssue(args));
3218
+ return text4(await updateLinearIssue(args));
2903
3219
  } catch (err) {
2904
- return fail3(err);
3220
+ return fail4(err);
2905
3221
  }
2906
3222
  }
2907
3223
  );
@@ -2909,14 +3225,14 @@ function registerLinearTools(server) {
2909
3225
  "linear_comment",
2910
3226
  "Add a markdown comment on a Linear issue (uuid or ENG-123).",
2911
3227
  {
2912
- id: z3.string(),
2913
- body: z3.string()
3228
+ id: z4.string(),
3229
+ body: z4.string()
2914
3230
  },
2915
3231
  async (args) => {
2916
3232
  try {
2917
- return text3(await commentLinearIssue(args));
3233
+ return text4(await commentLinearIssue(args));
2918
3234
  } catch (err) {
2919
- return fail3(err);
3235
+ return fail4(err);
2920
3236
  }
2921
3237
  }
2922
3238
  );
@@ -2924,6 +3240,7 @@ function registerLinearTools(server) {
2924
3240
 
2925
3241
  // src/mcp/issue-vendor-tools.ts
2926
3242
  function registerConnectedIssueVendorTools(server, settings = loadAppSettings()) {
3243
+ registerGithubIssueTools(server);
2927
3244
  if (isLinearConnected(settings)) registerLinearTools(server);
2928
3245
  if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
2929
3246
  }
@@ -2963,15 +3280,15 @@ function formatMcpPrList(input) {
2963
3280
  }
2964
3281
 
2965
3282
  // src/mcp/schedule-tools.ts
2966
- import { z as z4 } from "zod";
2967
- function text4(payload, isError = false) {
3283
+ import { z as z5 } from "zod";
3284
+ function text5(payload, isError = false) {
2968
3285
  return {
2969
3286
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
2970
3287
  ...isError ? { isError: true } : {}
2971
3288
  };
2972
3289
  }
2973
- function fail4(err) {
2974
- return text4(
3290
+ function fail5(err) {
3291
+ return text5(
2975
3292
  { error: err instanceof Error ? err.message : String(err) },
2976
3293
  true
2977
3294
  );
@@ -2996,9 +3313,9 @@ function registerScheduleTools(server) {
2996
3313
  {},
2997
3314
  async () => {
2998
3315
  try {
2999
- return text4({ schedules: listSchedules() });
3316
+ return text5({ schedules: listSchedules() });
3000
3317
  } catch (err) {
3001
- return fail4(err);
3318
+ return fail5(err);
3002
3319
  }
3003
3320
  }
3004
3321
  );
@@ -3006,24 +3323,24 @@ function registerScheduleTools(server) {
3006
3323
  "create_schedule",
3007
3324
  "Create a local schedule that, when due, sends a prompt to an orchestration chat (threadId) or starts a new Global orchestration chat (omit threadId). Pass threadId=self to continue this coordinator. Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without threadId open a new chat each run. Overnight/unattended runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate. Sideboard.app must be running for the job to fire.",
3008
3325
  {
3009
- prompt: z4.string().describe("User message / goal queued when the schedule fires"),
3010
- name: z4.string().optional(),
3011
- at: z4.string().optional().describe("ISO datetime for a one-shot"),
3012
- every: z4.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
3013
- cron: z4.string().optional().describe("5-field cron expression"),
3014
- tz: z4.string().optional().describe("IANA timezone for cron (default: system)"),
3015
- threadId: z4.string().optional().describe(
3326
+ prompt: z5.string().describe("User message / goal queued when the schedule fires"),
3327
+ name: z5.string().optional(),
3328
+ at: z5.string().optional().describe("ISO datetime for a one-shot"),
3329
+ every: z5.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
3330
+ cron: z5.string().optional().describe("5-field cron expression"),
3331
+ tz: z5.string().optional().describe("IANA timezone for cron (default: system)"),
3332
+ threadId: z5.string().optional().describe(
3016
3333
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
3017
3334
  ),
3018
- agent: z4.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
3019
- model: z4.string().optional()
3335
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
3336
+ model: z5.string().optional()
3020
3337
  },
3021
3338
  async (args) => {
3022
3339
  try {
3023
3340
  const when = parseWhen(args);
3024
3341
  const threadId = resolveScheduleThreadId(args.threadId);
3025
3342
  if (args.threadId?.trim().toLowerCase() === "self" && !threadId) {
3026
- return fail4(
3343
+ return fail5(
3027
3344
  new Error("threadId=self requires this turn to be an orchestration chat")
3028
3345
  );
3029
3346
  }
@@ -3036,12 +3353,12 @@ function registerScheduleTools(server) {
3036
3353
  model: args.model,
3037
3354
  createdBy: "mcp"
3038
3355
  });
3039
- return text4({
3356
+ return text5({
3040
3357
  schedule,
3041
3358
  hint: "Fires while Sideboard.app is running. Recurring jobs without threadId create a new orchestration chat each run. Overnight runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate."
3042
3359
  });
3043
3360
  } catch (err) {
3044
- return fail4(err);
3361
+ return fail5(err);
3045
3362
  }
3046
3363
  }
3047
3364
  );
@@ -3049,17 +3366,17 @@ function registerScheduleTools(server) {
3049
3366
  "update_schedule",
3050
3367
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
3051
3368
  {
3052
- id: z4.string(),
3053
- prompt: z4.string().optional(),
3054
- name: z4.string().optional(),
3055
- at: z4.string().optional(),
3056
- every: z4.string().optional(),
3057
- cron: z4.string().optional(),
3058
- tz: z4.string().optional(),
3059
- threadId: z4.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
3060
- agent: z4.enum(["claude", "cursor", "codex", "opencode"]).optional(),
3061
- model: z4.string().optional(),
3062
- enabled: z4.boolean().optional()
3369
+ id: z5.string(),
3370
+ prompt: z5.string().optional(),
3371
+ name: z5.string().optional(),
3372
+ at: z5.string().optional(),
3373
+ every: z5.string().optional(),
3374
+ cron: z5.string().optional(),
3375
+ tz: z5.string().optional(),
3376
+ threadId: z5.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
3377
+ agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional(),
3378
+ model: z5.string().optional(),
3379
+ enabled: z5.boolean().optional()
3063
3380
  },
3064
3381
  async (args) => {
3065
3382
  try {
@@ -3074,7 +3391,7 @@ function registerScheduleTools(server) {
3074
3391
  if (args.threadId !== void 0) {
3075
3392
  threadId = resolveScheduleThreadId(args.threadId);
3076
3393
  if (args.threadId.trim().toLowerCase() === "self" && !threadId) {
3077
- return fail4(
3394
+ return fail5(
3078
3395
  new Error("threadId=self requires this turn to be an orchestration chat")
3079
3396
  );
3080
3397
  }
@@ -3088,38 +3405,38 @@ function registerScheduleTools(server) {
3088
3405
  enabled: args.enabled,
3089
3406
  model: args.model
3090
3407
  });
3091
- return text4({ schedule });
3408
+ return text5({ schedule });
3092
3409
  } catch (err) {
3093
- return fail4(err);
3410
+ return fail5(err);
3094
3411
  }
3095
3412
  }
3096
3413
  );
3097
3414
  server.tool(
3098
3415
  "delete_schedule",
3099
3416
  "Delete a local schedule. Pass id from list_schedules.",
3100
- { id: z4.string() },
3417
+ { id: z5.string() },
3101
3418
  async ({ id }) => {
3102
3419
  try {
3103
3420
  deleteSchedule(id);
3104
- return text4({ ok: true, id });
3421
+ return text5({ ok: true, id });
3105
3422
  } catch (err) {
3106
- return fail4(err);
3423
+ return fail5(err);
3107
3424
  }
3108
3425
  }
3109
3426
  );
3110
3427
  server.tool(
3111
3428
  "run_schedule",
3112
3429
  "Fire a schedule now (does not wait for nextRunAt). Queues the prompt or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
3113
- { id: z4.string() },
3430
+ { id: z5.string() },
3114
3431
  async ({ id }) => {
3115
3432
  try {
3116
3433
  if (!getSchedule(id)) {
3117
- return fail4(new Error(`Schedule not found: ${id}`));
3434
+ return fail5(new Error(`Schedule not found: ${id}`));
3118
3435
  }
3119
3436
  const schedule = await fireSchedule(id);
3120
- return text4({ schedule });
3437
+ return text5({ schedule });
3121
3438
  } catch (err) {
3122
- return fail4(err);
3439
+ return fail5(err);
3123
3440
  }
3124
3441
  }
3125
3442
  );
@@ -3537,6 +3854,9 @@ async function startMcpServer() {
3537
3854
  version: "0.1.0"
3538
3855
  });
3539
3856
  const worktreeProfile = sideboardMcpProfile() === "worktree";
3857
+ if (worktreeProfile) {
3858
+ registerConnectedIssueVendorTools(server);
3859
+ }
3540
3860
  if (!worktreeProfile) {
3541
3861
  server.tool(
3542
3862
  "list_workspaces",
@@ -3589,13 +3909,13 @@ async function startMcpServer() {
3589
3909
  "list_board",
3590
3910
  "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.",
3591
3911
  {
3592
- query: z5.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
3593
- repoPath: z5.string().optional().describe("Limit to one workspace path from list_workspaces"),
3594
- kind: z5.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
3595
- column: z5.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
3912
+ query: z6.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
3913
+ repoPath: z6.string().optional().describe("Limit to one workspace path from list_workspaces"),
3914
+ kind: z6.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
3915
+ column: z6.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
3596
3916
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
3597
3917
  ),
3598
- limit: z5.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
3918
+ limit: z6.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
3599
3919
  },
3600
3920
  async ({ query, repoPath, kind, column, limit }) => {
3601
3921
  const workspaces = orch.listWorkspaces();
@@ -3633,7 +3953,7 @@ async function startMcpServer() {
3633
3953
  server.tool(
3634
3954
  "get_thread",
3635
3955
  "Get a compact thread summary by id/ref. Includes last message preview, parentThreadId, and child worktree threads (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
3636
- { ref: z5.string() },
3956
+ { ref: z6.string() },
3637
3957
  async ({ ref }) => {
3638
3958
  const t = orch.getThread(ref);
3639
3959
  if (!t) {
@@ -3675,17 +3995,17 @@ async function startMcpServer() {
3675
3995
  "present_artifact",
3676
3996
  "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.",
3677
3997
  {
3678
- title: z5.string().describe("Short title shown in the artifact pane header"),
3679
- type: z5.enum(["html", "svg", "markdown", "react", "log"]).describe(
3998
+ title: z6.string().describe("Short title shown in the artifact pane header"),
3999
+ type: z6.enum(["html", "svg", "markdown", "react", "log"]).describe(
3680
4000
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
3681
4001
  ),
3682
- content: z5.string().describe(
4002
+ content: z6.string().describe(
3683
4003
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
3684
4004
  ),
3685
- artifact_id: z5.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
3686
- status: z5.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
3687
- phase: z5.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
3688
- mode: z5.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
4005
+ artifact_id: z6.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
4006
+ status: z6.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
4007
+ phase: z6.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
4008
+ mode: z6.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
3689
4009
  },
3690
4010
  async ({ title, type, artifact_id, status, phase, mode }) => {
3691
4011
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -3706,15 +4026,15 @@ async function startMcpServer() {
3706
4026
  "ask_user",
3707
4027
  "Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
3708
4028
  {
3709
- questions: z5.array(
3710
- z5.object({
3711
- question: z5.string().describe("Full question text ending with ?"),
3712
- header: z5.string().max(24).optional().describe("Short label shown above the question"),
3713
- multiSelect: z5.boolean().optional().describe("Allow selecting multiple options"),
3714
- options: z5.array(
3715
- z5.object({
3716
- label: z5.string(),
3717
- description: z5.string().optional().describe("What this option means / when to choose it (strongly preferred)")
4029
+ questions: z6.array(
4030
+ z6.object({
4031
+ question: z6.string().describe("Full question text ending with ?"),
4032
+ header: z6.string().max(24).optional().describe("Short label shown above the question"),
4033
+ multiSelect: z6.boolean().optional().describe("Allow selecting multiple options"),
4034
+ options: z6.array(
4035
+ z6.object({
4036
+ label: z6.string(),
4037
+ description: z6.string().optional().describe("What this option means / when to choose it (strongly preferred)")
3718
4038
  })
3719
4039
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
3720
4040
  })
@@ -3733,9 +4053,9 @@ async function startMcpServer() {
3733
4053
  "present_plan",
3734
4054
  "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
3735
4055
  {
3736
- title: z5.string().optional().describe("Short plan title (defaults to Plan)"),
3737
- content: z5.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
3738
- thread_id: z5.string().optional().describe("Sideboard thread id when cwd is not the worktree")
4056
+ title: z6.string().optional().describe("Short plan title (defaults to Plan)"),
4057
+ content: z6.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
4058
+ thread_id: z6.string().optional().describe("Sideboard thread id when cwd is not the worktree")
3739
4059
  },
3740
4060
  async ({ title, content, thread_id }) => {
3741
4061
  const { writePlanFile: writePlanFile2 } = await import("./plan-file-LSSSQGTQ.js");
@@ -3758,15 +4078,15 @@ async function startMcpServer() {
3758
4078
  "present_schema",
3759
4079
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
3760
4080
  {
3761
- title: z5.string().describe("Pane title"),
3762
- mode: z5.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
3763
- datasource: z5.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
3764
- resource_id: z5.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
3765
- record_id: z5.string().optional().describe("Record id when opening form mode"),
3766
- resource: z5.record(z5.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
3767
- record: z5.record(z5.unknown()).optional().describe("Inline record { id, data, published_at? }"),
3768
- records: z5.array(z5.record(z5.unknown())).optional().describe("Inline records for table mode"),
3769
- pane_id: z5.string().optional().describe("Stable pane id across updates")
4081
+ title: z6.string().describe("Pane title"),
4082
+ mode: z6.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
4083
+ datasource: z6.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
4084
+ resource_id: z6.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
4085
+ record_id: z6.string().optional().describe("Record id when opening form mode"),
4086
+ resource: z6.record(z6.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
4087
+ record: z6.record(z6.unknown()).optional().describe("Inline record { id, data, published_at? }"),
4088
+ records: z6.array(z6.record(z6.unknown())).optional().describe("Inline records for table mode"),
4089
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
3770
4090
  },
3771
4091
  async (args) => {
3772
4092
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -3787,10 +4107,10 @@ async function startMcpServer() {
3787
4107
  "present_files",
3788
4108
  "Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
3789
4109
  {
3790
- title: z5.string().optional().describe("Pane title (default: Files)"),
3791
- datasource: z5.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
3792
- path: z5.string().optional().describe("Initial folder path (e.g. public)"),
3793
- pane_id: z5.string().optional().describe("Stable pane id across updates")
4110
+ title: z6.string().optional().describe("Pane title (default: Files)"),
4111
+ datasource: z6.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
4112
+ path: z6.string().optional().describe("Initial folder path (e.g. public)"),
4113
+ pane_id: z6.string().optional().describe("Stable pane id across updates")
3794
4114
  },
3795
4115
  async (args) => {
3796
4116
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -3817,7 +4137,7 @@ async function startMcpServer() {
3817
4137
  "set_caffeinate",
3818
4138
  "Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
3819
4139
  {
3820
- enabled: z5.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
4140
+ enabled: z6.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
3821
4141
  },
3822
4142
  async ({ enabled }) => {
3823
4143
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
@@ -3855,18 +4175,18 @@ async function startMcpServer() {
3855
4175
  "create_thread",
3856
4176
  `Create a worktree thread (chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, 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.`,
3857
4177
  {
3858
- sourceType: z5.enum(["branch", "pr", "ticket"]),
3859
- sourceRef: z5.string(),
3860
- agent: z5.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
3861
- model: z5.string().nullable().optional().describe(
4178
+ sourceType: z6.enum(["branch", "pr", "ticket"]),
4179
+ sourceRef: z6.string(),
4180
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
4181
+ model: z6.string().nullable().optional().describe(
3862
4182
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
3863
4183
  ),
3864
- repoPath: z5.string(),
3865
- title: z5.string().optional(),
3866
- cowboy: z5.boolean().optional().describe(
4184
+ repoPath: z6.string(),
4185
+ title: z6.string().optional(),
4186
+ cowboy: z6.boolean().optional().describe(
3867
4187
  "If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
3868
4188
  ),
3869
- parentThreadId: z5.string().optional().describe(
4189
+ parentThreadId: z6.string().optional().describe(
3870
4190
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
3871
4191
  )
3872
4192
  },
@@ -3882,10 +4202,10 @@ async function startMcpServer() {
3882
4202
  "start_board_card",
3883
4203
  "Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_thread.",
3884
4204
  {
3885
- kind: z5.enum(["ticket", "pr", "branch"]),
3886
- ref: z5.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
3887
- repoPath: z5.string().describe("Workspace path from list_workspaces / list_board"),
3888
- title: z5.string().optional()
4205
+ kind: z6.enum(["ticket", "pr", "branch"]),
4206
+ ref: z6.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
4207
+ repoPath: z6.string().describe("Workspace path from list_workspaces / list_board"),
4208
+ title: z6.string().optional()
3889
4209
  },
3890
4210
  async ({ kind, ref, repoPath, title }) => {
3891
4211
  const root = await resolveRepoRoot(repoPath);
@@ -3989,9 +4309,9 @@ async function startMcpServer() {
3989
4309
  "send_to_thread",
3990
4310
  'Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not force_stop to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that stops the child mid-thought. Call wait_for_turn again instead.',
3991
4311
  {
3992
- ref: z5.string(),
3993
- prompt: z5.string(),
3994
- force_stop: z5.boolean().optional()
4312
+ ref: z6.string(),
4313
+ prompt: z6.string(),
4314
+ force_stop: z6.boolean().optional()
3995
4315
  },
3996
4316
  async ({ ref, prompt, force_stop }) => {
3997
4317
  if (force_stop) {
@@ -4020,8 +4340,8 @@ async function startMcpServer() {
4020
4340
  "wait_for_turn",
4021
4341
  "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume a hang. On status error, lastError/text is the failure. On status stopped or broken, the child did not finish \u2014 resume with send_to_thread or tell the user; do not treat that as success. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
4022
4342
  {
4023
- ref: z5.string(),
4024
- timeoutMs: z5.number().optional()
4343
+ ref: z6.string(),
4344
+ timeoutMs: z6.number().optional()
4025
4345
  },
4026
4346
  async ({ ref, timeoutMs }) => {
4027
4347
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
@@ -4051,7 +4371,7 @@ async function startMcpServer() {
4051
4371
  server.tool(
4052
4372
  "get_turn_result",
4053
4373
  "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript. Includes usage for the last agent turn (tokens + costUsd when reported).",
4054
- { ref: z5.string() },
4374
+ { ref: z6.string() },
4055
4375
  async ({ ref }) => {
4056
4376
  const result = orch.getTurnResult(ref);
4057
4377
  return {
@@ -4072,8 +4392,8 @@ async function startMcpServer() {
4072
4392
  "stop_thread",
4073
4393
  "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
4074
4394
  {
4075
- ref: z5.string(),
4076
- force: z5.boolean().optional()
4395
+ ref: z6.string(),
4396
+ force: z6.boolean().optional()
4077
4397
  },
4078
4398
  async ({ ref, force }) => {
4079
4399
  const t = orch.getThread(ref);
@@ -4103,7 +4423,7 @@ async function startMcpServer() {
4103
4423
  server.tool(
4104
4424
  "archive_thread",
4105
4425
  "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
4106
- { ref: z5.string() },
4426
+ { ref: z6.string() },
4107
4427
  async ({ ref }) => {
4108
4428
  const t = orch.getThread(ref);
4109
4429
  if (!t) {
@@ -4136,7 +4456,7 @@ async function startMcpServer() {
4136
4456
  server.tool(
4137
4457
  "restore_thread",
4138
4458
  "Restore an archived thread (recreates worktree from branch when needed)",
4139
- { ref: z5.string() },
4459
+ { ref: z6.string() },
4140
4460
  async ({ ref }) => {
4141
4461
  try {
4142
4462
  const restored = await orch.restore(ref);
@@ -4162,8 +4482,8 @@ async function startMcpServer() {
4162
4482
  "get_diff",
4163
4483
  "Compact diff summary (capped hunks, paginated)",
4164
4484
  {
4165
- ref: z5.string(),
4166
- maxFiles: z5.number().optional()
4485
+ ref: z6.string(),
4486
+ maxFiles: z6.number().optional()
4167
4487
  },
4168
4488
  async ({ ref, maxFiles }) => {
4169
4489
  const summary = await orch.diffSummary(ref);
@@ -4183,7 +4503,7 @@ async function startMcpServer() {
4183
4503
  server.tool(
4184
4504
  "get_pr_checks",
4185
4505
  "Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
4186
- { ref: z5.string().describe("Worktree thread id/ref") },
4506
+ { ref: z6.string().describe("Worktree thread id/ref") },
4187
4507
  async ({ ref }) => {
4188
4508
  try {
4189
4509
  const checks = await orch.getPrChecks(ref);
@@ -4199,7 +4519,7 @@ async function startMcpServer() {
4199
4519
  server.tool(
4200
4520
  "request_review",
4201
4521
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
4202
- { ref: z5.string().describe("Worktree thread id/ref to review") },
4522
+ { ref: z6.string().describe("Worktree thread id/ref to review") },
4203
4523
  async ({ ref }) => {
4204
4524
  try {
4205
4525
  const tab = await orch.requestReview(ref);
@@ -4228,8 +4548,8 @@ async function startMcpServer() {
4228
4548
  "ask_git",
4229
4549
  "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd. If this tool errors that the GraphQL/PR body is too long, the branch is already pushed \u2014 have the worktree agent retry `gh pr create --body-file` with a short description (GitHub limit 65,536 characters). Do not invent SSH/auth failures from that error.",
4230
4550
  {
4231
- ref: z5.string().describe("Worktree thread id/ref"),
4232
- action: z5.enum(AGENT_GIT_ACTIONS).describe(
4551
+ ref: z6.string().describe("Worktree thread id/ref"),
4552
+ action: z6.enum(AGENT_GIT_ACTIONS).describe(
4233
4553
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
4234
4554
  )
4235
4555
  },
@@ -4274,7 +4594,7 @@ async function startMcpServer() {
4274
4594
  }
4275
4595
  }
4276
4596
  );
4277
- const agentEnum = z5.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
4597
+ const agentEnum = z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
4278
4598
  server.tool(
4279
4599
  "list_models",
4280
4600
  "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
@@ -4297,11 +4617,11 @@ async function startMcpServer() {
4297
4617
  "fork_worktree",
4298
4618
  "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
4299
4619
  {
4300
- ref: z5.string().describe("Worktree thread id/ref to fork"),
4301
- through_index: z5.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4620
+ ref: z6.string().describe("Worktree thread id/ref to fork"),
4621
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4302
4622
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
4303
- model: z5.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4304
- title: z5.string().optional()
4623
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4624
+ title: z6.string().optional()
4305
4625
  },
4306
4626
  async ({ ref, through_index, agent, model, title }) => {
4307
4627
  try {
@@ -4342,11 +4662,11 @@ async function startMcpServer() {
4342
4662
  "fork_chat",
4343
4663
  "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
4344
4664
  {
4345
- ref: z5.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
4346
- through_index: z5.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4665
+ ref: z6.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
4666
+ through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
4347
4667
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
4348
- model: z5.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4349
- title: z5.string().optional()
4668
+ model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
4669
+ title: z6.string().optional()
4350
4670
  },
4351
4671
  async ({ ref, through_index, agent, model, title }) => {
4352
4672
  try {
@@ -4392,8 +4712,8 @@ async function startMcpServer() {
4392
4712
  "run_dev_script",
4393
4713
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
4394
4714
  {
4395
- ref: z5.string(),
4396
- name: z5.string().optional()
4715
+ ref: z6.string(),
4716
+ name: z6.string().optional()
4397
4717
  },
4398
4718
  async ({ ref, name }) => {
4399
4719
  const result = await orch.startDev(ref, name);
@@ -4415,7 +4735,7 @@ async function startMcpServer() {
4415
4735
  server.tool(
4416
4736
  "list_run_scripts",
4417
4737
  "List named run scripts available for a thread",
4418
- { ref: z5.string() },
4738
+ { ref: z6.string() },
4419
4739
  async ({ ref }) => {
4420
4740
  const scripts = orch.listThreadRunScripts(ref);
4421
4741
  const active = orch.getActiveRuns(ref);
@@ -4433,8 +4753,8 @@ async function startMcpServer() {
4433
4753
  "stop_dev_script",
4434
4754
  "Stop a running script for a thread (all scripts if name omitted)",
4435
4755
  {
4436
- ref: z5.string(),
4437
- name: z5.string().optional()
4756
+ ref: z6.string(),
4757
+ name: z6.string().optional()
4438
4758
  },
4439
4759
  async ({ ref, name }) => {
4440
4760
  orch.stopDev(ref, name);
@@ -4444,7 +4764,7 @@ async function startMcpServer() {
4444
4764
  server.tool(
4445
4765
  "run_setup",
4446
4766
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
4447
- { ref: z5.string() },
4767
+ { ref: z6.string() },
4448
4768
  async ({ ref }) => {
4449
4769
  const result = await orch.runSetup(ref);
4450
4770
  return {
@@ -4455,7 +4775,7 @@ async function startMcpServer() {
4455
4775
  server.tool(
4456
4776
  "add_workspace",
4457
4777
  "Register a git repo as a Sideboard workspace",
4458
- { repoPath: z5.string() },
4778
+ { repoPath: z6.string() },
4459
4779
  async ({ repoPath }) => {
4460
4780
  const ws = await orch.addWorkspace(repoPath);
4461
4781
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -4464,7 +4784,7 @@ async function startMcpServer() {
4464
4784
  server.tool(
4465
4785
  "remove_workspace",
4466
4786
  "Unregister a Sideboard workspace (does not archive threads)",
4467
- { repoPath: z5.string() },
4787
+ { repoPath: z6.string() },
4468
4788
  async ({ repoPath }) => {
4469
4789
  orch.removeWorkspace(repoPath);
4470
4790
  return { content: [{ type: "text", text: "ok" }] };
@@ -4474,12 +4794,12 @@ async function startMcpServer() {
4474
4794
  "fanout",
4475
4795
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
4476
4796
  {
4477
- prompt: z5.string(),
4478
- agents: z5.array(z5.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
4479
- repoPath: z5.string(),
4480
- sourceType: z5.enum(["branch", "pr", "ticket"]).optional(),
4481
- sourceRef: z5.string().optional(),
4482
- title: z5.string().optional()
4797
+ prompt: z6.string(),
4798
+ agents: z6.array(z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
4799
+ repoPath: z6.string(),
4800
+ sourceType: z6.enum(["branch", "pr", "ticket"]).optional(),
4801
+ sourceRef: z6.string().optional(),
4802
+ title: z6.string().optional()
4483
4803
  },
4484
4804
  async (args) => {
4485
4805
  const threads = await orch.bestOfN(args);
@@ -4506,8 +4826,8 @@ async function startMcpServer() {
4506
4826
  "list_branches",
4507
4827
  "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
4508
4828
  {
4509
- repoPath: z5.string(),
4510
- unmergedOnly: z5.boolean().optional()
4829
+ repoPath: z6.string(),
4830
+ unmergedOnly: z6.boolean().optional()
4511
4831
  },
4512
4832
  async ({ repoPath, unmergedOnly }) => {
4513
4833
  const root = await resolveRepoRoot(repoPath);
@@ -4528,19 +4848,19 @@ async function startMcpServer() {
4528
4848
  "list_prs",
4529
4849
  'List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_workspaces. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled eng-review with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_thread with sourceType=pr.',
4530
4850
  {
4531
- repoPath: z5.string(),
4532
- query: z5.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
4533
- queue: z5.enum(["review", "mine", "approved", "changes"]).optional().describe(
4851
+ repoPath: z6.string(),
4852
+ query: z6.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
4853
+ queue: z6.enum(["review", "mine", "approved", "changes"]).optional().describe(
4534
4854
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
4535
4855
  ),
4536
- state: z5.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
4537
- label: z5.string().optional().describe(
4856
+ state: z6.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
4857
+ label: z6.string().optional().describe(
4538
4858
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
4539
4859
  ),
4540
- reviewer: z5.string().optional().describe(
4860
+ reviewer: z6.string().optional().describe(
4541
4861
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
4542
4862
  ),
4543
- limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
4863
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
4544
4864
  },
4545
4865
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
4546
4866
  const root = await resolveRepoRoot(repoPath);
@@ -4572,7 +4892,7 @@ async function startMcpServer() {
4572
4892
  server.tool(
4573
4893
  "get_pr_stack",
4574
4894
  "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
4575
- { ref: z5.string() },
4895
+ { ref: z6.string() },
4576
4896
  async ({ ref }) => {
4577
4897
  const stack = await orch.getPrStack(ref);
4578
4898
  return {
@@ -4584,8 +4904,8 @@ async function startMcpServer() {
4584
4904
  "open_pr_stack_layers",
4585
4905
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
4586
4906
  {
4587
- ref: z5.string(),
4588
- layer: z5.number().int().positive().optional()
4907
+ ref: z6.string(),
4908
+ layer: z6.number().int().positive().optional()
4589
4909
  },
4590
4910
  async ({ ref, layer }) => {
4591
4911
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -4619,9 +4939,9 @@ async function startMcpServer() {
4619
4939
  "add_stack_layer",
4620
4940
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
4621
4941
  {
4622
- ref: z5.string(),
4623
- branchName: z5.string(),
4624
- title: z5.string().optional()
4942
+ ref: z6.string(),
4943
+ branchName: z6.string(),
4944
+ title: z6.string().optional()
4625
4945
  },
4626
4946
  async ({ ref, branchName, title }) => {
4627
4947
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -4650,11 +4970,11 @@ async function startMcpServer() {
4650
4970
  "create_pr_stack",
4651
4971
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
4652
4972
  {
4653
- repoPath: z5.string(),
4654
- branches: z5.array(z5.string()).min(1),
4655
- agent: z5.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
4656
- base: z5.string().optional(),
4657
- title: z5.string().optional()
4973
+ repoPath: z6.string(),
4974
+ branches: z6.array(z6.string()).min(1),
4975
+ agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
4976
+ base: z6.string().optional(),
4977
+ title: z6.string().optional()
4658
4978
  },
4659
4979
  async (args) => {
4660
4980
  const result = await orch.createPrStack({
@@ -4693,10 +5013,10 @@ async function startMcpServer() {
4693
5013
  "list_issues",
4694
5014
  "List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Then create_thread with sourceType=ticket.",
4695
5015
  {
4696
- repoPath: z5.string(),
4697
- query: z5.string().optional().describe("Search title, identifier, or description"),
4698
- assignee: z5.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
4699
- limit: z5.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
5016
+ repoPath: z6.string(),
5017
+ query: z6.string().optional().describe("Search title, identifier, or description"),
5018
+ assignee: z6.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
5019
+ limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
4700
5020
  },
4701
5021
  async ({ repoPath, query, assignee, limit }) => {
4702
5022
  const root = await resolveRepoRoot(repoPath);
@@ -4828,8 +5148,8 @@ var BrightsySideboardApi = class {
4828
5148
  };
4829
5149
  function taskMessageText(task) {
4830
5150
  const parts = task.message?.parts ?? [];
4831
- const text5 = parts.find((p) => p.kind === "text")?.text ?? "";
4832
- return text5.trim();
5151
+ const text6 = parts.find((p) => p.kind === "text")?.text ?? "";
5152
+ return text6.trim();
4833
5153
  }
4834
5154
 
4835
5155
  // src/brightsy/cloud-connect.ts
@@ -5302,11 +5622,11 @@ async function startSlackOAuth(opts) {
5302
5622
  import { WebSocket as WsWebSocket } from "ws";
5303
5623
  var BACKOFF_START_MS = 1e3;
5304
5624
  var BACKOFF_MAX_MS = 3e4;
5305
- function stripSlackMentions(text5) {
5306
- return text5.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
5625
+ function stripSlackMentions(text6) {
5626
+ return text6.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
5307
5627
  }
5308
- function isSlackStopCommand(text5) {
5309
- const t = stripSlackMentions(text5).toLowerCase();
5628
+ function isSlackStopCommand(text6) {
5629
+ const t = stripSlackMentions(text6).toLowerCase();
5310
5630
  return t === "stop" || t === "sideboard_force_stop";
5311
5631
  }
5312
5632
  function parseSlackSocketFrame(raw) {
@@ -5328,8 +5648,8 @@ function inboundFromSocketFrame(frame) {
5328
5648
  const ts = event.ts?.trim();
5329
5649
  if (!channelId || !ts) return null;
5330
5650
  const rawText = event.text?.trim() ?? "";
5331
- const text5 = stripSlackMentions(rawText);
5332
- if (!text5) return null;
5651
+ const text6 = stripSlackMentions(rawText);
5652
+ if (!text6) return null;
5333
5653
  const teamId = (frame.payload?.team_id || event.team || "").trim();
5334
5654
  if (!teamId) return null;
5335
5655
  const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
@@ -5341,7 +5661,7 @@ function inboundFromSocketFrame(frame) {
5341
5661
  ts,
5342
5662
  threadTs: event.thread_ts?.trim() || void 0,
5343
5663
  userId: event.user?.trim(),
5344
- text: text5,
5664
+ text: text6,
5345
5665
  kind: isMention ? "mention" : "dm"
5346
5666
  };
5347
5667
  }
@@ -5881,12 +6201,12 @@ function formatSlackInboundPrompt(msg) {
5881
6201
 
5882
6202
  ${msg.text}`;
5883
6203
  }
5884
- function isSlackInboundUserPrompt(text5) {
5885
- return text5.startsWith("Slack DM\n") || text5.startsWith("Slack @mention\n");
6204
+ function isSlackInboundUserPrompt(text6) {
6205
+ return text6.startsWith("Slack DM\n") || text6.startsWith("Slack @mention\n");
5886
6206
  }
5887
- function formatSlackSignedReply(deviceLabel, text5) {
6207
+ function formatSlackSignedReply(deviceLabel, text6) {
5888
6208
  const label = deviceLabel.trim();
5889
- const body = text5.trim();
6209
+ const body = text6.trim();
5890
6210
  if (!body) return body;
5891
6211
  if (!label) return body;
5892
6212
  const head = `${label}:`;
@@ -5895,8 +6215,8 @@ function formatSlackSignedReply(deviceLabel, text5) {
5895
6215
  }
5896
6216
  return `${label}: ${body}`;
5897
6217
  }
5898
- function signForThisMac(text5) {
5899
- return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text5);
6218
+ function signForThisMac(text6) {
6219
+ return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text6);
5900
6220
  }
5901
6221
  function slackReplyThreadTs(msg) {
5902
6222
  if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
@@ -5957,9 +6277,9 @@ function replyStub(target) {
5957
6277
  kind: "dm"
5958
6278
  };
5959
6279
  }
5960
- async function postSlackText(target, text5, opts) {
6280
+ async function postSlackText(target, text6, opts) {
5961
6281
  if (opts.postReply) {
5962
- const result = await opts.postReply(replyStub(target), text5);
6282
+ const result = await opts.postReply(replyStub(target), text6);
5963
6283
  const ts2 = result && typeof result === "object" && typeof result.ts === "string" ? result.ts.trim() : "";
5964
6284
  return ts2 ? { ts: ts2 } : null;
5965
6285
  }
@@ -5969,7 +6289,7 @@ async function postSlackText(target, text5, opts) {
5969
6289
  "chat.postMessage",
5970
6290
  {
5971
6291
  channel: target.channelId,
5972
- text: text5,
6292
+ text: text6,
5973
6293
  thread_ts: target.threadTs
5974
6294
  },
5975
6295
  opts.fetchImpl
@@ -5977,9 +6297,9 @@ async function postSlackText(target, text5, opts) {
5977
6297
  const ts = json.ts?.trim();
5978
6298
  return ts ? { ts } : null;
5979
6299
  }
5980
- async function updateSlackText(target, ts, text5, opts) {
6300
+ async function updateSlackText(target, ts, text6, opts) {
5981
6301
  if (opts.updateReply) {
5982
- await opts.updateReply(replyStub(target), ts, text5);
6302
+ await opts.updateReply(replyStub(target), ts, text6);
5983
6303
  return;
5984
6304
  }
5985
6305
  const token = writeTokenForTeam(target.teamId);
@@ -5989,7 +6309,7 @@ async function updateSlackText(target, ts, text5, opts) {
5989
6309
  {
5990
6310
  channel: target.channelId,
5991
6311
  ts,
5992
- text: text5
6312
+ text: text6
5993
6313
  },
5994
6314
  opts.fetchImpl
5995
6315
  );
@@ -6017,8 +6337,8 @@ function replyTargetOf(msg) {
6017
6337
  threadTs: slackReplyThreadTs(msg)
6018
6338
  };
6019
6339
  }
6020
- async function postSlackReply(msg, text5, opts) {
6021
- await postSlackText(replyTargetOf(msg), signForThisMac(text5), opts);
6340
+ async function postSlackReply(msg, text6, opts) {
6341
+ await postSlackText(replyTargetOf(msg), signForThisMac(text6), opts);
6022
6342
  }
6023
6343
  function startSlackTurnProgress(msg, threadId, opts) {
6024
6344
  const log = opts.onLog ?? (() => void 0);
@@ -6101,10 +6421,10 @@ function startSlackTurnProgress(msg, threadId, opts) {
6101
6421
  }
6102
6422
  });
6103
6423
  },
6104
- finishWith: (text5) => {
6424
+ finishWith: (text6) => {
6105
6425
  stop();
6106
6426
  return run2(async () => {
6107
- const signed = signForThisMac(text5.trim());
6427
+ const signed = signForThisMac(text6.trim());
6108
6428
  if (!signed) {
6109
6429
  if (!postedTs) return;
6110
6430
  const ts = postedTs;
@@ -6132,11 +6452,11 @@ function startSlackTurnProgress(msg, threadId, opts) {
6132
6452
  };
6133
6453
  }
6134
6454
  var lastRelayed = /* @__PURE__ */ new Map();
6135
- function markRelayed(threadId, text5) {
6136
- lastRelayed.set(threadId, `${threadId}:${text5}`);
6455
+ function markRelayed(threadId, text6) {
6456
+ lastRelayed.set(threadId, `${threadId}:${text6}`);
6137
6457
  }
6138
- function alreadyRelayed(threadId, text5) {
6139
- return lastRelayed.get(threadId) === `${threadId}:${text5}`;
6458
+ function alreadyRelayed(threadId, text6) {
6459
+ return lastRelayed.get(threadId) === `${threadId}:${text6}`;
6140
6460
  }
6141
6461
  async function relayCoordinatorReplyToSlack(threadId, opts) {
6142
6462
  const target = getSlackReplyTarget(threadId);
@@ -6145,14 +6465,14 @@ async function relayCoordinatorReplyToSlack(threadId, opts) {
6145
6465
  const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
6146
6466
  if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
6147
6467
  const result = getOrchestrator().getTurnResult(threadId);
6148
- const text5 = result.text.trim();
6149
- if (!text5) return;
6150
- if (alreadyRelayed(threadId, text5)) return;
6151
- markRelayed(threadId, text5);
6468
+ const text6 = result.text.trim();
6469
+ if (!text6) return;
6470
+ if (alreadyRelayed(threadId, text6)) return;
6471
+ markRelayed(threadId, text6);
6152
6472
  const log = opts.onLog ?? (() => void 0);
6153
6473
  try {
6154
- await postSlackText(target, signForThisMac(text5), opts);
6155
- log(`replied ${target.threadTs ?? target.channelId} (${text5.length} chars)`);
6474
+ await postSlackText(target, signForThisMac(text6), opts);
6475
+ log(`replied ${target.threadTs ?? target.channelId} (${text6.length} chars)`);
6156
6476
  } catch (err) {
6157
6477
  lastRelayed.delete(threadId);
6158
6478
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -6400,10 +6720,10 @@ function resolveSlackListenMode(opts) {
6400
6720
  }
6401
6721
 
6402
6722
  // src/slack/relay-hub.ts
6403
- function parseSlackDeviceDestination(text5) {
6404
- const m = text5.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
6405
- if (!m) return { label: null, rest: text5 };
6406
- return { label: m[1], rest: text5.slice(m[0].length) };
6723
+ function parseSlackDeviceDestination(text6) {
6724
+ const m = text6.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
6725
+ if (!m) return { label: null, rest: text6 };
6726
+ return { label: m[1], rest: text6.slice(m[0].length) };
6407
6727
  }
6408
6728
  var SlackRelayHub = class {
6409
6729
  sessions = /* @__PURE__ */ new Map();
@@ -7009,6 +7329,9 @@ export {
7009
7329
  SlackOAuthCancelledError,
7010
7330
  SlackRelayHub,
7011
7331
  THINKING_EFFORTS,
7332
+ WORKTREE_ABLETIME_MCP_TOOLS,
7333
+ WORKTREE_GITHUB_MCP_TOOLS,
7334
+ WORKTREE_LINEAR_MCP_TOOLS,
7012
7335
  WORKTREE_MCP_TOOLS,
7013
7336
  abletimeMcpRequest,
7014
7337
  abletimeMcpUrl,
@@ -7096,6 +7419,8 @@ export {
7096
7419
  codexUnattendedGitConfigArgs,
7097
7420
  coerceOrchestratorAgent,
7098
7421
  collectTakenTeamSlugs,
7422
+ commentAbleTimeTask,
7423
+ commentGitHubIssue,
7099
7424
  commentLinearIssue,
7100
7425
  commitAll,
7101
7426
  computeNextRunAt,
@@ -7117,6 +7442,7 @@ export {
7117
7442
  createChatTab,
7118
7443
  createEmptyThread,
7119
7444
  createExistingBranchWorktree,
7445
+ createGitHubIssue,
7120
7446
  createGlobalChat,
7121
7447
  createLinearIssue,
7122
7448
  createLinearPkce,
@@ -7203,6 +7529,10 @@ export {
7203
7529
  formatGhLandError,
7204
7530
  formatGitAuthModeDirective,
7205
7531
  formatIpcInvokeError,
7532
+ formatIssueToolsDirective,
7533
+ formatIssueToolsReminder,
7534
+ formatLinearDirective,
7535
+ formatLinearReminder,
7206
7536
  formatLongRunningDirective,
7207
7537
  formatLongRunningReminder,
7208
7538
  formatMergePrError,
@@ -7248,6 +7578,7 @@ export {
7248
7578
  getDefaultRunScript,
7249
7579
  getDiff,
7250
7580
  getDiffSummary,
7581
+ getGitHubIssue,
7251
7582
  getGitHubStatus,
7252
7583
  getGithubGitAuthMode,
7253
7584
  getGithubPat,
@@ -7350,6 +7681,7 @@ export {
7350
7681
  issueAttachmentForAbleTimeTask,
7351
7682
  issueMatchesAssignee,
7352
7683
  issueSourceLabel,
7684
+ issueTicketFromThread,
7353
7685
  lastRequestOccupancy,
7354
7686
  latestPendingPlanQuestions,
7355
7687
  linearAuthorizationHeader,
@@ -7357,6 +7689,7 @@ export {
7357
7689
  linearGraphql,
7358
7690
  linearOAuthAuthorizeUrl,
7359
7691
  linearOAuthCredentials,
7692
+ linearTicketIdFromThread,
7360
7693
  listAbleTimeAssignedIssues,
7361
7694
  listAbleTimeProjects,
7362
7695
  listAbleTimeTasks,
@@ -7443,6 +7776,7 @@ export {
7443
7776
  parseDurationMs,
7444
7777
  parseForceStopMessage,
7445
7778
  parseGhStackViewJson,
7779
+ parseGitHubIssueNumber,
7446
7780
  parseGithubSlugFromRemoteUrl,
7447
7781
  parseMcpList,
7448
7782
  parsePlanQuestionsInput,
@@ -7506,6 +7840,7 @@ export {
7506
7840
  resolveFilesToCopy,
7507
7841
  resolveGhAuthToken,
7508
7842
  resolveGitDirsForLockRecovery,
7843
+ resolveGitHubIssueRepo,
7509
7844
  resolveGithubAgentToken,
7510
7845
  resolveGithubRepoSlug,
7511
7846
  resolveLinearState,
@@ -7610,6 +7945,7 @@ export {
7610
7945
  threadsDir,
7611
7946
  threadsSharingWorktree,
7612
7947
  toAbleTimeIssueInfo,
7948
+ toGitHubIssueInfo,
7613
7949
  toPublicAppSettings,
7614
7950
  toolActivityLine,
7615
7951
  toolDescription,
@@ -7617,6 +7953,7 @@ export {
7617
7953
  toolFilePath,
7618
7954
  totalTokens,
7619
7955
  turnCostUsdFromCursorUsage,
7956
+ updateAbleTimeTask,
7620
7957
  updateAdvancedSettings,
7621
7958
  updateAgentExecutable,
7622
7959
  updateAppEnvironment,
@@ -7624,6 +7961,7 @@ export {
7624
7961
  updateClaudeSettings,
7625
7962
  updateCodexSettings,
7626
7963
  updateDefaultsSettings,
7964
+ updateGitHubIssue,
7627
7965
  updateIntegrationsSettings,
7628
7966
  updateLinearIssue,
7629
7967
  updateOpencodeSettings,