@sideboard-ai/core 0.1.190 → 0.1.192

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-J3EGHIAV.js → abletime-53T7G667.js} +3 -1
  2. package/dist/{abletime-CAFQ4GEG.js → abletime-JFMBCSIQ.js} +3 -1
  3. package/dist/{agents-5OBZETUV.js → agents-T5VBB6X3.js} +2 -2
  4. package/dist/{agents-DTPVTDB5.js → agents-TXGQIK2E.js} +2 -2
  5. package/dist/{chunk-TEJU6UJY.js → chunk-4M2AK3O5.js} +126 -20
  6. package/dist/{chunk-XXEIF2IL.js → chunk-7NSXGL6L.js} +3 -3
  7. package/dist/{chunk-IAOXW6RI.js → chunk-FZ3G5TXM.js} +23 -16
  8. package/dist/{chunk-Z6TW3A5P.js → chunk-NF6ENGHS.js} +24 -17
  9. package/dist/{chunk-OLDZXO53.js → chunk-TWSCTJCR.js} +2 -2
  10. package/dist/{chunk-KEVY4GEK.js → chunk-UBQIELFS.js} +123 -20
  11. package/dist/{chunk-VIODP5PT.js → chunk-VV7UT3E2.js} +3 -3
  12. package/dist/{chunk-GBWKMWUQ.js → chunk-WHD4JNTN.js} +2 -2
  13. package/dist/{coordinator-prompt-LWKOL3AZ.js → coordinator-prompt-PNUDN4DW.js} +1 -1
  14. package/dist/{coordinator-prompt-MSC44OOM.js → coordinator-prompt-S4HHFSLI.js} +1 -1
  15. package/dist/{global-workspace-6G5JJH2E.js → global-workspace-2BVCJUAL.js} +1 -1
  16. package/dist/{global-workspace-XATJTV3N.js → global-workspace-JTBBH2K5.js} +1 -1
  17. package/dist/index.cjs +565 -57
  18. package/dist/index.d.cts +96 -6
  19. package/dist/index.d.ts +96 -6
  20. package/dist/index.js +422 -30
  21. package/dist/mcp/run-stdio.cjs +543 -57
  22. package/dist/mcp/run-stdio.js +407 -30
  23. package/dist/{orchestrator-BHJELEFM.js → orchestrator-4G76BOHH.js} +5 -3
  24. package/dist/{orchestrator-HWNGCDGW.js → orchestrator-LWEZ74RK.js} +5 -3
  25. package/dist/{workspaces-LMFCRXTT.js → workspaces-OY7D5RPU.js} +1 -1
  26. package/dist/{workspaces-FWKPWSXV.js → workspaces-ZYPME3YD.js} +1 -1
  27. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -197,6 +197,7 @@ import {
197
197
  resolveCreateFirstPrompt,
198
198
  resolveDetachedJobScript,
199
199
  resolveFilesToCopy,
200
+ resolveOrchChildFollowUp,
200
201
  resolveReviewGuidelines,
201
202
  resolveScheduleThreadId,
202
203
  resolveSendFollowUp,
@@ -238,7 +239,7 @@ import {
238
239
  worktreeCleanupSettings,
239
240
  wrapReviewSkillMarkdown,
240
241
  writeWorktreeFile
241
- } from "./chunk-Z6TW3A5P.js";
242
+ } from "./chunk-NF6ENGHS.js";
242
243
  import {
243
244
  LEGACY_PLAN_FILE_REL,
244
245
  PLAN_FILE_NAME,
@@ -344,7 +345,7 @@ import {
344
345
  withEventParentId,
345
346
  withEventsParentId,
346
347
  writeInjectedMcpConfig
347
- } from "./chunk-OLDZXO53.js";
348
+ } from "./chunk-TWSCTJCR.js";
348
349
  import {
349
350
  CLOUD_COORDINATOR_BUSY_REPLY,
350
351
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -387,7 +388,7 @@ import {
387
388
  stageBuffersAsAttachments,
388
389
  syncWorkspacesFromThreads,
389
390
  takenTeamSlugsForOrchestration
390
- } from "./chunk-VIODP5PT.js";
391
+ } from "./chunk-VV7UT3E2.js";
391
392
  import {
392
393
  FAMOUS_SOCCER_TEAMS,
393
394
  GITHUB_PR_BODY_MAX_CHARS,
@@ -514,9 +515,13 @@ import {
514
515
  } from "./chunk-AN3THU7F.js";
515
516
  import {
516
517
  ABLETIME_MCP_PATH,
518
+ ABLETIME_MCP_PM_PATH,
519
+ ABLETIME_PM_TOOL_NAMES,
517
520
  DEFAULT_ABLETIME_HOST,
521
+ ableTimeDependencyTarget,
518
522
  abletimeMcpRequest,
519
523
  abletimeMcpUrl,
524
+ abletimeToolUsesPm,
520
525
  callAbleTimeTool,
521
526
  commentAbleTimeTask,
522
527
  createAbleTimeTask,
@@ -535,7 +540,7 @@ import {
535
540
  toAbleTimeIssueInfo,
536
541
  updateAbleTimeTask,
537
542
  verifyAbleTimeConnection
538
- } from "./chunk-TEJU6UJY.js";
543
+ } from "./chunk-4M2AK3O5.js";
539
544
  import {
540
545
  formatFetchError,
541
546
  httpFetch,
@@ -1159,12 +1164,14 @@ var ISSUE_FIELDS = `
1159
1164
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
1160
1165
  relations(first: 25) {
1161
1166
  nodes {
1167
+ id
1162
1168
  type
1163
1169
  relatedIssue { ${ISSUE_REF_FIELDS} }
1164
1170
  }
1165
1171
  }
1166
1172
  inverseRelations(first: 25) {
1167
1173
  nodes {
1174
+ id
1168
1175
  type
1169
1176
  issue { ${ISSUE_REF_FIELDS} }
1170
1177
  }
@@ -1284,6 +1291,45 @@ mutation SideboardCommentCreate($input: CommentCreateInput!) {
1284
1291
  }
1285
1292
  }
1286
1293
  `;
1294
+ var LABELS_QUERY = `
1295
+ query SideboardLabels($first: Int!) {
1296
+ issueLabels(first: $first) {
1297
+ nodes { id name }
1298
+ }
1299
+ }
1300
+ `;
1301
+ var TEAM_LABELS_QUERY = `
1302
+ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
1303
+ team(id: $id) {
1304
+ labels(first: $first, after: $after) {
1305
+ nodes { id name }
1306
+ pageInfo { hasNextPage endCursor }
1307
+ }
1308
+ }
1309
+ }
1310
+ `;
1311
+ var PROJECTS_QUERY = `
1312
+ query SideboardProjects($first: Int!, $filter: ProjectFilter) {
1313
+ projects(first: $first, filter: $filter) {
1314
+ nodes { id name slugId }
1315
+ }
1316
+ }
1317
+ `;
1318
+ var ISSUE_RELATION_CREATE = `
1319
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
1320
+ issueRelationCreate(input: $input) {
1321
+ success
1322
+ issueRelation { id type }
1323
+ }
1324
+ }
1325
+ `;
1326
+ var ISSUE_RELATION_DELETE = `
1327
+ mutation SideboardIssueRelationDelete($id: String!) {
1328
+ issueRelationDelete(id: $id) {
1329
+ success
1330
+ }
1331
+ }
1332
+ `;
1287
1333
  function rewriteLinearError(message) {
1288
1334
  if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
1289
1335
  return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
@@ -1403,12 +1449,18 @@ function mapRelations(node) {
1403
1449
  for (const rel of node.relations?.nodes ?? []) {
1404
1450
  const issue = mapIssueRef(rel.relatedIssue);
1405
1451
  if (!issue) continue;
1406
- out.push({ type: String(rel.type ?? "related"), issue });
1452
+ const id = rel.id?.trim();
1453
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
1407
1454
  }
1408
1455
  for (const rel of node.inverseRelations?.nodes ?? []) {
1409
1456
  const issue = mapIssueRef(rel.issue);
1410
1457
  if (!issue) continue;
1411
- out.push({ type: flipLinearRelationType(String(rel.type ?? "related")), issue });
1458
+ const id = rel.id?.trim();
1459
+ out.push({
1460
+ type: flipLinearRelationType(String(rel.type ?? "related")),
1461
+ issue,
1462
+ ...id ? { id } : {}
1463
+ });
1412
1464
  }
1413
1465
  return out;
1414
1466
  }
@@ -1569,6 +1621,171 @@ function resolveLinearCycle(team, cycle) {
1569
1621
  }
1570
1622
  return found.id;
1571
1623
  }
1624
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
1625
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1626
+ function isLinearNoneToken(value) {
1627
+ if (value == null) return true;
1628
+ const s = value.trim();
1629
+ return !s || LINEAR_NONE_RE.test(s);
1630
+ }
1631
+ function resolveLinearLabel(labels, name) {
1632
+ const s = name.trim();
1633
+ if (!s) throw new Error("Linear label is empty");
1634
+ const lower = s.toLowerCase();
1635
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
1636
+ if (!found) {
1637
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1638
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
1639
+ }
1640
+ return found;
1641
+ }
1642
+ function resolveLinearProject(projects, project) {
1643
+ if (isLinearNoneToken(project)) return null;
1644
+ const s = (project ?? "").trim();
1645
+ const lower = s.toLowerCase();
1646
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
1647
+ if (!found) {
1648
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1649
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
1650
+ }
1651
+ return found.id;
1652
+ }
1653
+ function normalizeLinearRelationType(type) {
1654
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
1655
+ if (s === "blocks" || s === "block") return "blocks";
1656
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
1657
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
1658
+ if (s === "duplicate" || s === "dup") return "duplicate";
1659
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
1660
+ if (s === "similar") return "similar";
1661
+ throw new Error(
1662
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
1663
+ );
1664
+ }
1665
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
1666
+ const view = normalizeLinearRelationType(type);
1667
+ if (view === "blockedBy") {
1668
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
1669
+ }
1670
+ if (view === "duplicateOf") {
1671
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
1672
+ }
1673
+ return { issueId: thisIssueId, relatedIssueId, type: view };
1674
+ }
1675
+ function relationMatches(rel, type, issueRef) {
1676
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
1677
+ const ref = issueRef.trim().toLowerCase();
1678
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
1679
+ }
1680
+ function formatRelationList(relations) {
1681
+ if (!relations.length) return "(none)";
1682
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
1683
+ }
1684
+ function mapLabelNodes(nodes) {
1685
+ return (nodes ?? []).flatMap((node) => {
1686
+ const id = String(node.id ?? "").trim();
1687
+ const name = String(node.name ?? "").trim();
1688
+ if (!id) return [];
1689
+ return [{ id, name }];
1690
+ });
1691
+ }
1692
+ async function listLinearWorkspaceLabels(opts) {
1693
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
1694
+ return mapLabelNodes(json.issueLabels?.nodes);
1695
+ }
1696
+ async function listLinearLabels(opts) {
1697
+ const teamId = opts?.teamId?.trim();
1698
+ if (teamId) {
1699
+ const out = [];
1700
+ let after;
1701
+ for (let page = 0; page < 4; page++) {
1702
+ const json = await linearGraphql(
1703
+ TEAM_LABELS_QUERY,
1704
+ after ? { id: teamId, first: 250, after } : { id: teamId, first: 250 },
1705
+ opts
1706
+ );
1707
+ const conn = json.team?.labels;
1708
+ out.push(...mapLabelNodes(conn?.nodes));
1709
+ if (!conn?.pageInfo?.hasNextPage || !conn.pageInfo.endCursor) break;
1710
+ after = conn.pageInfo.endCursor;
1711
+ }
1712
+ if (out.length) return out;
1713
+ }
1714
+ return listLinearWorkspaceLabels(opts);
1715
+ }
1716
+ async function listLinearProjects(filter, opts) {
1717
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
1718
+ return (json.projects?.nodes ?? []).flatMap((node) => {
1719
+ const id = String(node.id ?? "").trim();
1720
+ if (!id) return [];
1721
+ return [
1722
+ {
1723
+ id,
1724
+ name: String(node.name ?? ""),
1725
+ ...node.slugId ? { slugId: String(node.slugId) } : {}
1726
+ }
1727
+ ];
1728
+ });
1729
+ }
1730
+ async function resolveLinearProjectId(project, opts) {
1731
+ if (isLinearNoneToken(project)) return null;
1732
+ const s = (project ?? "").trim();
1733
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
1734
+ const matched = await listLinearProjects(filter, opts);
1735
+ try {
1736
+ return resolveLinearProject(matched, s);
1737
+ } catch {
1738
+ if (UUID_RE.test(s)) return s;
1739
+ const listed = await listLinearProjects(void 0, opts);
1740
+ return resolveLinearProject(listed, s);
1741
+ }
1742
+ }
1743
+ async function resolveLinearLabelIds(names, opts) {
1744
+ if (names.length === 0) return [];
1745
+ const labels = await listLinearLabels(opts);
1746
+ return names.map((name) => {
1747
+ const s = name.trim();
1748
+ try {
1749
+ return resolveLinearLabel(labels, s).id;
1750
+ } catch (error) {
1751
+ if (UUID_RE.test(s)) return s;
1752
+ throw error;
1753
+ }
1754
+ });
1755
+ }
1756
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
1757
+ const current = [...issue.relations];
1758
+ for (const rel of remove ?? []) {
1759
+ const type = normalizeLinearRelationType(rel.type);
1760
+ const issueRef = rel.issue.trim();
1761
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
1762
+ const match = current.find((r) => relationMatches(r, type, issueRef));
1763
+ if (!match?.id) {
1764
+ throw new Error(
1765
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
1766
+ );
1767
+ }
1768
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
1769
+ if (!json.issueRelationDelete?.success) {
1770
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
1771
+ }
1772
+ const idx = current.indexOf(match);
1773
+ if (idx >= 0) current.splice(idx, 1);
1774
+ }
1775
+ for (const rel of add ?? []) {
1776
+ const type = normalizeLinearRelationType(rel.type);
1777
+ const issueRef = rel.issue.trim();
1778
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
1779
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
1780
+ const related = await getLinearIssue(issueRef, opts);
1781
+ const input = linearRelationCreateInput(issue.id, type, related.id);
1782
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
1783
+ if (!json.issueRelationCreate?.success) {
1784
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
1785
+ }
1786
+ current.push({ type, issue: related, id: void 0 });
1787
+ }
1788
+ }
1572
1789
  function normalizePriority(priority) {
1573
1790
  if (priority == null) return void 0;
1574
1791
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -1576,7 +1793,6 @@ function normalizePriority(priority) {
1576
1793
  }
1577
1794
  return priority;
1578
1795
  }
1579
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1580
1796
  function assigneeFilterKey(assignee) {
1581
1797
  return (assignee ?? "").trim();
1582
1798
  }
@@ -1789,8 +2005,10 @@ async function updateLinearIssue(input, opts) {
1789
2005
  if (input.description !== void 0) {
1790
2006
  mutationInput.description = input.description;
1791
2007
  }
1792
- const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
1793
- const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
2008
+ const addRelations = input.relations?.length ? input.relations : void 0;
2009
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
2010
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || input.labels !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
2011
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
1794
2012
  if (input.state?.trim()) {
1795
2013
  const { teams } = await listLinearTeams(opts);
1796
2014
  const teamRef = existing?.team?.key || existing?.team?.id;
@@ -1823,14 +2041,47 @@ async function updateLinearIssue(input, opts) {
1823
2041
  if (input.priority !== void 0) {
1824
2042
  mutationInput.priority = normalizePriority(input.priority);
1825
2043
  }
1826
- if (Object.keys(mutationInput).length === 0) {
1827
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
2044
+ if (input.labels !== void 0) {
2045
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, {
2046
+ ...opts,
2047
+ teamId: existing?.team?.id
2048
+ });
2049
+ }
2050
+ if (input.project !== void 0) {
2051
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
2052
+ }
2053
+ if (input.parent !== void 0) {
2054
+ if (isLinearNoneToken(input.parent)) {
2055
+ mutationInput.parentId = null;
2056
+ } else {
2057
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
2058
+ mutationInput.parentId = parent.id;
2059
+ }
2060
+ }
2061
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
2062
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
2063
+ if (!hasIssueFields && !hasRelationWrites) {
2064
+ throw new Error(
2065
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
2066
+ );
2067
+ }
2068
+ let updated;
2069
+ if (hasIssueFields) {
2070
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
2071
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
2072
+ throw new Error("Linear issueUpdate failed");
2073
+ }
2074
+ updated = mapIssue(json.issueUpdate.issue);
1828
2075
  }
1829
- const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1830
- if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1831
- throw new Error("Linear issueUpdate failed");
2076
+ if (hasRelationWrites) {
2077
+ const base = updated ?? existing;
2078
+ if (!base) {
2079
+ throw new Error(`Linear issue not found: ${issueId}`);
2080
+ }
2081
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
2082
+ return getLinearIssue(base.id || issueId, opts);
1832
2083
  }
1833
- return mapIssue(json.issueUpdate.issue);
2084
+ return updated;
1834
2085
  }
1835
2086
  async function commentLinearIssue(input, opts) {
1836
2087
  const issueId = input.id.trim();
@@ -2050,13 +2301,78 @@ async function commentGitHubIssue(input, opts) {
2050
2301
  const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
2051
2302
  return { body, url };
2052
2303
  }
2304
+ function isGitHubNoneToken(value) {
2305
+ if (value == null) return true;
2306
+ const s = value.trim();
2307
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
2308
+ }
2309
+ function githubRelationEditFlag(type, remove = false) {
2310
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
2311
+ if (s === "blockedby" || s === "blocked" || s === "dependson") {
2312
+ return remove ? "--remove-blocked-by" : "--add-blocked-by";
2313
+ }
2314
+ if (s === "blocks" || s === "blocking") {
2315
+ return remove ? "--remove-blocking" : "--add-blocking";
2316
+ }
2317
+ throw new Error(
2318
+ `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
2319
+ );
2320
+ }
2321
+ function labelsEqual(a, b) {
2322
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
2323
+ }
2324
+ function parseProjectTitles(raw) {
2325
+ const out = [];
2326
+ const seen = /* @__PURE__ */ new Set();
2327
+ const push = (name) => {
2328
+ const title = name.trim();
2329
+ const key = title.toLowerCase();
2330
+ if (!title || seen.has(key)) return;
2331
+ seen.add(key);
2332
+ out.push(title);
2333
+ };
2334
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
2335
+ if (Array.isArray(fromItems)) {
2336
+ for (const item of fromItems) {
2337
+ if (typeof item === "string") {
2338
+ push(item);
2339
+ continue;
2340
+ }
2341
+ if (!item || typeof item !== "object") continue;
2342
+ const rec = item;
2343
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
2344
+ const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
2345
+ if (title) push(title);
2346
+ }
2347
+ }
2348
+ return out;
2349
+ }
2350
+ async function listGitHubIssueProjects(number, cwd, repoArgs) {
2351
+ const result = await gh(
2352
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
2353
+ cwd,
2354
+ { reject: false }
2355
+ );
2356
+ if (result.exitCode !== 0 || !result.stdout.trim()) return [];
2357
+ try {
2358
+ const parsed = JSON.parse(result.stdout);
2359
+ return parseProjectTitles(parsed);
2360
+ } catch {
2361
+ return [];
2362
+ }
2363
+ }
2053
2364
  async function updateGitHubIssue(input, opts) {
2054
2365
  const number = parseGitHubIssueNumber(input.id);
2055
2366
  const title = input.title?.trim();
2056
2367
  const body = input.body;
2057
2368
  const state = input.state?.trim().toLowerCase();
2058
- if (!title && body === void 0 && !state) {
2059
- throw new Error("github_update_issue needs at least one of title, body, state");
2369
+ const addRelations = input.relations?.length ? input.relations : void 0;
2370
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
2371
+ const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
2372
+ if (!hasEditFields && !state) {
2373
+ throw new Error(
2374
+ "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
2375
+ );
2060
2376
  }
2061
2377
  const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
2062
2378
  if (state === "closed" || state === "close") {
@@ -2072,10 +2388,46 @@ async function updateGitHubIssue(input, opts) {
2072
2388
  } else if (state) {
2073
2389
  throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
2074
2390
  }
2075
- if (title || body !== void 0) {
2076
- const args = ["issue", "edit", String(number), ...repoArgs];
2077
- if (title) args.push("--title", title);
2078
- if (body !== void 0) args.push("--body", body);
2391
+ const args = ["issue", "edit", String(number), ...repoArgs];
2392
+ if (title) args.push("--title", title);
2393
+ if (body !== void 0) args.push("--body", body);
2394
+ if (input.labels !== void 0) {
2395
+ const existing = await getGitHubIssue(String(number), opts);
2396
+ const desired = input.labels.map((name) => name.trim()).filter(Boolean);
2397
+ const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
2398
+ const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
2399
+ if (add.length) args.push("--add-label", add.join(","));
2400
+ if (remove.length) args.push("--remove-label", remove.join(","));
2401
+ }
2402
+ if (input.project !== void 0) {
2403
+ const current = await listGitHubIssueProjects(number, cwd, repoArgs);
2404
+ if (isGitHubNoneToken(input.project)) {
2405
+ for (const name of current) args.push("--remove-project", name);
2406
+ } else {
2407
+ const wanted = (input.project ?? "").trim();
2408
+ if (!current.some((name) => labelsEqual(name, wanted))) {
2409
+ args.push("--add-project", wanted);
2410
+ }
2411
+ for (const name of current) {
2412
+ if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
2413
+ }
2414
+ }
2415
+ }
2416
+ if (input.parent !== void 0) {
2417
+ if (isGitHubNoneToken(input.parent)) {
2418
+ args.push("--remove-parent");
2419
+ } else {
2420
+ args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
2421
+ }
2422
+ }
2423
+ for (const rel of removeRelations ?? []) {
2424
+ args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
2425
+ }
2426
+ for (const rel of addRelations ?? []) {
2427
+ args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
2428
+ }
2429
+ const editChanged = args.length > 3 + repoArgs.length;
2430
+ if (editChanged) {
2079
2431
  requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
2080
2432
  }
2081
2433
  return getGitHubIssue(String(number), opts);
@@ -3671,14 +4023,23 @@ function registerAbleTimeTools(server) {
3671
4023
  }
3672
4024
  }
3673
4025
  );
4026
+ const relationInputSchema = z4.object({
4027
+ type: z4.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
4028
+ issue: z4.string().describe("Related task id or reference (CRM-232).")
4029
+ });
3674
4030
  server.tool(
3675
4031
  "abletime_update_task",
3676
- "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.",
4032
+ 'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
3677
4033
  {
3678
4034
  id: z4.string(),
3679
4035
  title: z4.string().optional(),
3680
4036
  description: z4.string().optional(),
3681
- state: z4.string().optional()
4037
+ state: z4.string().optional(),
4038
+ labels: z4.array(z4.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
4039
+ project: z4.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
4040
+ parent: z4.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
4041
+ relations: z4.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
4042
+ removeRelations: z4.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
3682
4043
  },
3683
4044
  async (args) => {
3684
4045
  try {
@@ -3812,14 +4173,23 @@ function registerGithubIssueTools(server) {
3812
4173
  }
3813
4174
  }
3814
4175
  );
4176
+ const relationInputSchema = z5.object({
4177
+ type: z5.string().describe("blocks or blockedBy (this issue's view)."),
4178
+ issue: z5.string().describe("Related issue #123 or URL.")
4179
+ });
3815
4180
  server.tool(
3816
4181
  "github_update_issue",
3817
- "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.",
4182
+ 'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
3818
4183
  {
3819
4184
  id: z5.string(),
3820
4185
  title: z5.string().optional(),
3821
4186
  body: z5.string().optional(),
3822
4187
  state: z5.string().optional().describe("open or closed"),
4188
+ labels: z5.array(z5.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
4189
+ project: z5.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
4190
+ parent: z5.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
4191
+ relations: z5.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
4192
+ removeRelations: z5.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
3823
4193
  repoPath: repoPathSchema
3824
4194
  },
3825
4195
  async (args) => {
@@ -3949,9 +4319,13 @@ function registerLinearTools(server) {
3949
4319
  }
3950
4320
  }
3951
4321
  );
4322
+ const relationInputSchema = z6.object({
4323
+ type: z6.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
4324
+ issue: z6.string().describe("Related issue uuid or identifier (ENG-123).")
4325
+ });
3952
4326
  server.tool(
3953
4327
  "linear_update_issue",
3954
- 'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, and/or cycle. Cycle is name, number, "current"/"active", or "none" to unschedule. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
4328
+ 'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
3955
4329
  {
3956
4330
  id: z6.string(),
3957
4331
  title: z6.string().optional(),
@@ -3959,7 +4333,12 @@ function registerLinearTools(server) {
3959
4333
  state: z6.string().optional(),
3960
4334
  assignee: z6.string().nullable().optional(),
3961
4335
  priority: prioritySchema,
3962
- cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
4336
+ cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
4337
+ labels: z6.array(z6.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
4338
+ project: z6.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
4339
+ parent: z6.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
4340
+ relations: z6.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
4341
+ removeRelations: z6.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
3963
4342
  },
3964
4343
  async (args) => {
3965
4344
  try {
@@ -5194,7 +5573,7 @@ async function startMcpServer() {
5194
5573
  );
5195
5574
  server.tool(
5196
5575
  "send_to_thread",
5197
- '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.',
5576
+ 'Steer a prompt on a worktree thread chat (Settings \u2192 Follow-up, default steer: interrupt and start now, skip the inbox). 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 send_to_thread to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
5198
5577
  {
5199
5578
  ref: z9.string(),
5200
5579
  prompt: z9.string(),
@@ -5207,7 +5586,9 @@ async function startMcpServer() {
5207
5586
  orch.stop(ref, { clearQueue: true, notifyParent: false });
5208
5587
  }
5209
5588
  }
5210
- const thread = await orch.send(ref, prompt);
5589
+ const thread = await orch.send(ref, prompt, {
5590
+ followUp: resolveOrchChildFollowUp()
5591
+ });
5211
5592
  return mcpJson({
5212
5593
  id: thread.id,
5213
5594
  status: thread.status,
@@ -5218,7 +5599,7 @@ async function startMcpServer() {
5218
5599
  );
5219
5600
  server.tool(
5220
5601
  "wait_for_turn",
5221
- "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).",
5602
+ "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_to_thread a check-in (that steers / interrupts), 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).",
5222
5603
  {
5223
5604
  ref: z9.string(),
5224
5605
  timeoutMs: z9.number().optional()
@@ -5375,7 +5756,7 @@ async function startMcpServer() {
5375
5756
  );
5376
5757
  server.tool(
5377
5758
  "ask_git",
5378
- "Commit & push, open a draft PR, mark ready for review, 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=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). 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.",
5759
+ "Commit & push, open a draft PR, mark ready for review, 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, steers the worktree agent to commit (Settings \u2192 Follow-up, default interrupt now); 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=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). 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.",
5379
5760
  {
5380
5761
  ref: z9.string().describe("Worktree thread id/ref"),
5381
5762
  action: z9.enum(AGENT_GIT_ACTIONS).describe(
@@ -7997,6 +8378,8 @@ async function startSlackRelayServer(opts) {
7997
8378
  }
7998
8379
  export {
7999
8380
  ABLETIME_MCP_PATH,
8381
+ ABLETIME_MCP_PM_PATH,
8382
+ ABLETIME_PM_TOOL_NAMES,
8000
8383
  AGENT_GIT_ACTIONS,
8001
8384
  AGENT_RUNNER_MAX_OLD_SPACE_MB,
8002
8385
  ATTACHMENTS_DIR,
@@ -8086,8 +8469,10 @@ export {
8086
8469
  WORKTREE_GITHUB_MCP_TOOLS,
8087
8470
  WORKTREE_LINEAR_MCP_TOOLS,
8088
8471
  WORKTREE_MCP_TOOLS,
8472
+ ableTimeDependencyTarget,
8089
8473
  abletimeMcpRequest,
8090
8474
  abletimeMcpUrl,
8475
+ abletimeToolUsesPm,
8091
8476
  ackSlackInboundSeen,
8092
8477
  addBoardPin,
8093
8478
  addPrStackLayer,
@@ -8360,6 +8745,7 @@ export {
8360
8745
  git,
8361
8746
  githubAgentGitEnv,
8362
8747
  githubHttpsInsteadOfEntries,
8748
+ githubRelationEditFlag,
8363
8749
  globalAgentCwd,
8364
8750
  groupHomeBoardWorktrees,
8365
8751
  handleSlackInbound,
@@ -8409,6 +8795,7 @@ export {
8409
8795
  isInternalAgentStatusText,
8410
8796
  isIssueSourceConnected,
8411
8797
  isLinearConnected,
8798
+ isLinearNoneToken,
8412
8799
  isLinearOAuthCancelled,
8413
8800
  isOptionalServiceId,
8414
8801
  isOrchestratorCapableAgent,
@@ -8446,6 +8833,7 @@ export {
8446
8833
  linearGraphql,
8447
8834
  linearOAuthAuthorizeUrl,
8448
8835
  linearOAuthCredentials,
8836
+ linearRelationCreateInput,
8449
8837
  linearTicketIdFromThread,
8450
8838
  listAbleTimeAssignedIssues,
8451
8839
  listAbleTimeProjects,
@@ -8514,6 +8902,7 @@ export {
8514
8902
  nonInteractiveGitProcessEnv,
8515
8903
  normalizeAbleTimeHost,
8516
8904
  normalizeCodeSelection,
8905
+ normalizeLinearRelationType,
8517
8906
  normalizeParseResult,
8518
8907
  normalizeServiceOrigin,
8519
8908
  normalizeThinkingEffort,
@@ -8607,11 +8996,14 @@ export {
8607
8996
  resolveGithubAgentToken,
8608
8997
  resolveGithubRepoSlug,
8609
8998
  resolveLinearCycle,
8999
+ resolveLinearLabel,
9000
+ resolveLinearProject,
8610
9001
  resolveLinearState,
8611
9002
  resolveLinearTeam,
8612
9003
  resolveLoginCommand,
8613
9004
  resolveNewOrchestratorOptions,
8614
9005
  resolveNewThreadOptions,
9006
+ resolveOrchChildFollowUp,
8615
9007
  resolveOrchestratorDefaults,
8616
9008
  resolvePlanMarkdown,
8617
9009
  resolvePrSelector,