@sideboard-ai/core 0.1.189 → 0.1.191

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.
package/dist/index.js CHANGED
@@ -238,7 +238,7 @@ import {
238
238
  worktreeCleanupSettings,
239
239
  wrapReviewSkillMarkdown,
240
240
  writeWorktreeFile
241
- } from "./chunk-U6FCBOS7.js";
241
+ } from "./chunk-5RRAKGPK.js";
242
242
  import {
243
243
  LEGACY_PLAN_FILE_REL,
244
244
  PLAN_FILE_NAME,
@@ -344,7 +344,7 @@ import {
344
344
  withEventParentId,
345
345
  withEventsParentId,
346
346
  writeInjectedMcpConfig
347
- } from "./chunk-ISOCLQEX.js";
347
+ } from "./chunk-OLDZXO53.js";
348
348
  import {
349
349
  CLOUD_COORDINATOR_BUSY_REPLY,
350
350
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -514,9 +514,13 @@ import {
514
514
  } from "./chunk-AN3THU7F.js";
515
515
  import {
516
516
  ABLETIME_MCP_PATH,
517
+ ABLETIME_MCP_PM_PATH,
518
+ ABLETIME_PM_TOOL_NAMES,
517
519
  DEFAULT_ABLETIME_HOST,
520
+ ableTimeDependencyTarget,
518
521
  abletimeMcpRequest,
519
522
  abletimeMcpUrl,
523
+ abletimeToolUsesPm,
520
524
  callAbleTimeTool,
521
525
  commentAbleTimeTask,
522
526
  createAbleTimeTask,
@@ -535,7 +539,7 @@ import {
535
539
  toAbleTimeIssueInfo,
536
540
  updateAbleTimeTask,
537
541
  verifyAbleTimeConnection
538
- } from "./chunk-TEJU6UJY.js";
542
+ } from "./chunk-4M2AK3O5.js";
539
543
  import {
540
544
  formatFetchError,
541
545
  httpFetch,
@@ -553,7 +557,7 @@ import {
553
557
  trustSystemCertificates,
554
558
  turnCostUsdFromCursorUsage,
555
559
  withMaxOldSpaceSize
556
- } from "./chunk-ZSFQLR7Y.js";
560
+ } from "./chunk-FY64F2ZQ.js";
557
561
  import {
558
562
  FOLLOW_UP_BEHAVIORS,
559
563
  GITHUB_GIT_AUTH_MODES,
@@ -1159,12 +1163,14 @@ var ISSUE_FIELDS = `
1159
1163
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
1160
1164
  relations(first: 25) {
1161
1165
  nodes {
1166
+ id
1162
1167
  type
1163
1168
  relatedIssue { ${ISSUE_REF_FIELDS} }
1164
1169
  }
1165
1170
  }
1166
1171
  inverseRelations(first: 25) {
1167
1172
  nodes {
1173
+ id
1168
1174
  type
1169
1175
  issue { ${ISSUE_REF_FIELDS} }
1170
1176
  }
@@ -1284,6 +1290,35 @@ mutation SideboardCommentCreate($input: CommentCreateInput!) {
1284
1290
  }
1285
1291
  }
1286
1292
  `;
1293
+ var LABELS_QUERY = `
1294
+ query SideboardLabels($first: Int!) {
1295
+ issueLabels(first: $first) {
1296
+ nodes { id name }
1297
+ }
1298
+ }
1299
+ `;
1300
+ var PROJECTS_QUERY = `
1301
+ query SideboardProjects($first: Int!, $filter: ProjectFilter) {
1302
+ projects(first: $first, filter: $filter) {
1303
+ nodes { id name slugId }
1304
+ }
1305
+ }
1306
+ `;
1307
+ var ISSUE_RELATION_CREATE = `
1308
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
1309
+ issueRelationCreate(input: $input) {
1310
+ success
1311
+ issueRelation { id type }
1312
+ }
1313
+ }
1314
+ `;
1315
+ var ISSUE_RELATION_DELETE = `
1316
+ mutation SideboardIssueRelationDelete($id: String!) {
1317
+ issueRelationDelete(id: $id) {
1318
+ success
1319
+ }
1320
+ }
1321
+ `;
1287
1322
  function rewriteLinearError(message) {
1288
1323
  if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
1289
1324
  return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
@@ -1403,12 +1438,18 @@ function mapRelations(node) {
1403
1438
  for (const rel of node.relations?.nodes ?? []) {
1404
1439
  const issue = mapIssueRef(rel.relatedIssue);
1405
1440
  if (!issue) continue;
1406
- out.push({ type: String(rel.type ?? "related"), issue });
1441
+ const id = rel.id?.trim();
1442
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
1407
1443
  }
1408
1444
  for (const rel of node.inverseRelations?.nodes ?? []) {
1409
1445
  const issue = mapIssueRef(rel.issue);
1410
1446
  if (!issue) continue;
1411
- out.push({ type: flipLinearRelationType(String(rel.type ?? "related")), issue });
1447
+ const id = rel.id?.trim();
1448
+ out.push({
1449
+ type: flipLinearRelationType(String(rel.type ?? "related")),
1450
+ issue,
1451
+ ...id ? { id } : {}
1452
+ });
1412
1453
  }
1413
1454
  return out;
1414
1455
  }
@@ -1554,7 +1595,14 @@ function resolveLinearCycle(team, cycle) {
1554
1595
  }
1555
1596
  const lower = s.toLowerCase();
1556
1597
  const asNumber = Number.parseInt(s, 10);
1557
- const cycles = team.cycles ?? [];
1598
+ const seen = /* @__PURE__ */ new Set();
1599
+ const cycles = [team.activeCycle, ...team.cycles ?? []].filter(
1600
+ (c) => {
1601
+ if (!c?.id || seen.has(c.id)) return false;
1602
+ seen.add(c.id);
1603
+ return true;
1604
+ }
1605
+ );
1558
1606
  const found = cycles.find((c) => c.id === s) || cycles.find((c) => c.name.toLowerCase() === lower) || (Number.isInteger(asNumber) ? cycles.find((c) => c.number === asNumber) : void 0);
1559
1607
  if (!found) {
1560
1608
  const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
@@ -1562,6 +1610,148 @@ function resolveLinearCycle(team, cycle) {
1562
1610
  }
1563
1611
  return found.id;
1564
1612
  }
1613
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
1614
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1615
+ function isLinearNoneToken(value) {
1616
+ if (value == null) return true;
1617
+ const s = value.trim();
1618
+ return !s || LINEAR_NONE_RE.test(s);
1619
+ }
1620
+ function resolveLinearLabel(labels, name) {
1621
+ const s = name.trim();
1622
+ if (!s) throw new Error("Linear label is empty");
1623
+ const lower = s.toLowerCase();
1624
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
1625
+ if (!found) {
1626
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1627
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
1628
+ }
1629
+ return found;
1630
+ }
1631
+ function resolveLinearProject(projects, project) {
1632
+ if (isLinearNoneToken(project)) return null;
1633
+ const s = (project ?? "").trim();
1634
+ const lower = s.toLowerCase();
1635
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
1636
+ if (!found) {
1637
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1638
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
1639
+ }
1640
+ return found.id;
1641
+ }
1642
+ function normalizeLinearRelationType(type) {
1643
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
1644
+ if (s === "blocks" || s === "block") return "blocks";
1645
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
1646
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
1647
+ if (s === "duplicate" || s === "dup") return "duplicate";
1648
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
1649
+ if (s === "similar") return "similar";
1650
+ throw new Error(
1651
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
1652
+ );
1653
+ }
1654
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
1655
+ const view = normalizeLinearRelationType(type);
1656
+ if (view === "blockedBy") {
1657
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
1658
+ }
1659
+ if (view === "duplicateOf") {
1660
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
1661
+ }
1662
+ return { issueId: thisIssueId, relatedIssueId, type: view };
1663
+ }
1664
+ function relationMatches(rel, type, issueRef) {
1665
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
1666
+ const ref = issueRef.trim().toLowerCase();
1667
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
1668
+ }
1669
+ function formatRelationList(relations) {
1670
+ if (!relations.length) return "(none)";
1671
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
1672
+ }
1673
+ async function listLinearLabels(opts) {
1674
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
1675
+ return (json.issueLabels?.nodes ?? []).flatMap((node) => {
1676
+ const id = String(node.id ?? "").trim();
1677
+ const name = String(node.name ?? "").trim();
1678
+ if (!id) return [];
1679
+ return [{ id, name }];
1680
+ });
1681
+ }
1682
+ async function listLinearProjects(filter, opts) {
1683
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
1684
+ return (json.projects?.nodes ?? []).flatMap((node) => {
1685
+ const id = String(node.id ?? "").trim();
1686
+ if (!id) return [];
1687
+ return [
1688
+ {
1689
+ id,
1690
+ name: String(node.name ?? ""),
1691
+ ...node.slugId ? { slugId: String(node.slugId) } : {}
1692
+ }
1693
+ ];
1694
+ });
1695
+ }
1696
+ async function resolveLinearProjectId(project, opts) {
1697
+ if (isLinearNoneToken(project)) return null;
1698
+ const s = (project ?? "").trim();
1699
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
1700
+ const matched = await listLinearProjects(filter, opts);
1701
+ try {
1702
+ return resolveLinearProject(matched, s);
1703
+ } catch {
1704
+ if (UUID_RE.test(s)) return s;
1705
+ const listed = await listLinearProjects(void 0, opts);
1706
+ return resolveLinearProject(listed, s);
1707
+ }
1708
+ }
1709
+ async function resolveLinearLabelIds(names, opts) {
1710
+ if (names.length === 0) return [];
1711
+ const labels = await listLinearLabels(opts);
1712
+ return names.map((name) => {
1713
+ const s = name.trim();
1714
+ try {
1715
+ return resolveLinearLabel(labels, s).id;
1716
+ } catch (error) {
1717
+ if (UUID_RE.test(s)) return s;
1718
+ throw error;
1719
+ }
1720
+ });
1721
+ }
1722
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
1723
+ const current = [...issue.relations];
1724
+ for (const rel of remove ?? []) {
1725
+ const type = normalizeLinearRelationType(rel.type);
1726
+ const issueRef = rel.issue.trim();
1727
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
1728
+ const match = current.find((r) => relationMatches(r, type, issueRef));
1729
+ if (!match?.id) {
1730
+ throw new Error(
1731
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
1732
+ );
1733
+ }
1734
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
1735
+ if (!json.issueRelationDelete?.success) {
1736
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
1737
+ }
1738
+ const idx = current.indexOf(match);
1739
+ if (idx >= 0) current.splice(idx, 1);
1740
+ }
1741
+ for (const rel of add ?? []) {
1742
+ const type = normalizeLinearRelationType(rel.type);
1743
+ const issueRef = rel.issue.trim();
1744
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
1745
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
1746
+ const related = await getLinearIssue(issueRef, opts);
1747
+ const input = linearRelationCreateInput(issue.id, type, related.id);
1748
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
1749
+ if (!json.issueRelationCreate?.success) {
1750
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
1751
+ }
1752
+ current.push({ type, issue: related, id: void 0 });
1753
+ }
1754
+ }
1565
1755
  function normalizePriority(priority) {
1566
1756
  if (priority == null) return void 0;
1567
1757
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -1569,7 +1759,6 @@ function normalizePriority(priority) {
1569
1759
  }
1570
1760
  return priority;
1571
1761
  }
1572
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1573
1762
  function assigneeFilterKey(assignee) {
1574
1763
  return (assignee ?? "").trim();
1575
1764
  }
@@ -1671,7 +1860,12 @@ async function listLinearCommentsSince(opts) {
1671
1860
  first,
1672
1861
  filter: {
1673
1862
  createdAt: { gte: opts.since },
1674
- issue: buildLinearIssueFilter({ assignee, query: query || void 0 })
1863
+ issue: buildLinearIssueFilter({
1864
+ assignee,
1865
+ // Search hits may match identifier/comments — do not also require
1866
+ // title/description contains when an allowlist is already applied.
1867
+ query: restrictToIssues ? void 0 : query || void 0
1868
+ })
1675
1869
  }
1676
1870
  },
1677
1871
  opts
@@ -1777,8 +1971,10 @@ async function updateLinearIssue(input, opts) {
1777
1971
  if (input.description !== void 0) {
1778
1972
  mutationInput.description = input.description;
1779
1973
  }
1780
- const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
1781
- const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
1974
+ const addRelations = input.relations?.length ? input.relations : void 0;
1975
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
1976
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
1977
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
1782
1978
  if (input.state?.trim()) {
1783
1979
  const { teams } = await listLinearTeams(opts);
1784
1980
  const teamRef = existing?.team?.key || existing?.team?.id;
@@ -1811,14 +2007,44 @@ async function updateLinearIssue(input, opts) {
1811
2007
  if (input.priority !== void 0) {
1812
2008
  mutationInput.priority = normalizePriority(input.priority);
1813
2009
  }
1814
- if (Object.keys(mutationInput).length === 0) {
1815
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
2010
+ if (input.labels !== void 0) {
2011
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, opts);
2012
+ }
2013
+ if (input.project !== void 0) {
2014
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
2015
+ }
2016
+ if (input.parent !== void 0) {
2017
+ if (isLinearNoneToken(input.parent)) {
2018
+ mutationInput.parentId = null;
2019
+ } else {
2020
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
2021
+ mutationInput.parentId = parent.id;
2022
+ }
2023
+ }
2024
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
2025
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
2026
+ if (!hasIssueFields && !hasRelationWrites) {
2027
+ throw new Error(
2028
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
2029
+ );
1816
2030
  }
1817
- const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1818
- if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1819
- throw new Error("Linear issueUpdate failed");
2031
+ let updated;
2032
+ if (hasIssueFields) {
2033
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
2034
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
2035
+ throw new Error("Linear issueUpdate failed");
2036
+ }
2037
+ updated = mapIssue(json.issueUpdate.issue);
1820
2038
  }
1821
- return mapIssue(json.issueUpdate.issue);
2039
+ if (hasRelationWrites) {
2040
+ const base = updated ?? existing;
2041
+ if (!base) {
2042
+ throw new Error(`Linear issue not found: ${issueId}`);
2043
+ }
2044
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
2045
+ return getLinearIssue(base.id || issueId, opts);
2046
+ }
2047
+ return updated;
1822
2048
  }
1823
2049
  async function commentLinearIssue(input, opts) {
1824
2050
  const issueId = input.id.trim();
@@ -1970,7 +2196,7 @@ async function listGitHubIssueCommentsSince(opts) {
1970
2196
  }).filter((n) => n != null)
1971
2197
  );
1972
2198
  if (restrictToIssues && allowed.size === 0) return [];
1973
- const pageSize = Math.min(GITHUB_ISSUE_COMMENTS_PAGE_SIZE, Math.max(1, limit));
2199
+ const pageSize = GITHUB_ISSUE_COMMENTS_PAGE_SIZE;
1974
2200
  const out = [];
1975
2201
  for (let page = 1; page <= GITHUB_ISSUE_COMMENTS_MAX_PAGES; page++) {
1976
2202
  const path2 = `repos/${slug}/issues/comments?since=${encodeURIComponent(opts.since)}&sort=created&direction=desc&per_page=${pageSize}&page=${page}`;
@@ -2038,13 +2264,78 @@ async function commentGitHubIssue(input, opts) {
2038
2264
  const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
2039
2265
  return { body, url };
2040
2266
  }
2267
+ function isGitHubNoneToken(value) {
2268
+ if (value == null) return true;
2269
+ const s = value.trim();
2270
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
2271
+ }
2272
+ function githubRelationEditFlag(type, remove = false) {
2273
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
2274
+ if (s === "blockedby" || s === "blocked" || s === "dependson") {
2275
+ return remove ? "--remove-blocked-by" : "--add-blocked-by";
2276
+ }
2277
+ if (s === "blocks" || s === "blocking") {
2278
+ return remove ? "--remove-blocking" : "--add-blocking";
2279
+ }
2280
+ throw new Error(
2281
+ `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
2282
+ );
2283
+ }
2284
+ function labelsEqual(a, b) {
2285
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
2286
+ }
2287
+ function parseProjectTitles(raw) {
2288
+ const out = [];
2289
+ const seen = /* @__PURE__ */ new Set();
2290
+ const push = (name) => {
2291
+ const title = name.trim();
2292
+ const key = title.toLowerCase();
2293
+ if (!title || seen.has(key)) return;
2294
+ seen.add(key);
2295
+ out.push(title);
2296
+ };
2297
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
2298
+ if (Array.isArray(fromItems)) {
2299
+ for (const item of fromItems) {
2300
+ if (typeof item === "string") {
2301
+ push(item);
2302
+ continue;
2303
+ }
2304
+ if (!item || typeof item !== "object") continue;
2305
+ const rec = item;
2306
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
2307
+ const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
2308
+ if (title) push(title);
2309
+ }
2310
+ }
2311
+ return out;
2312
+ }
2313
+ async function listGitHubIssueProjects(number, cwd, repoArgs) {
2314
+ const result = await gh(
2315
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems,projects"],
2316
+ cwd,
2317
+ { reject: false }
2318
+ );
2319
+ if (result.exitCode !== 0 || !result.stdout.trim()) return [];
2320
+ try {
2321
+ const parsed = JSON.parse(result.stdout);
2322
+ return parseProjectTitles(parsed);
2323
+ } catch {
2324
+ return [];
2325
+ }
2326
+ }
2041
2327
  async function updateGitHubIssue(input, opts) {
2042
2328
  const number = parseGitHubIssueNumber(input.id);
2043
2329
  const title = input.title?.trim();
2044
2330
  const body = input.body;
2045
2331
  const state = input.state?.trim().toLowerCase();
2046
- if (!title && body === void 0 && !state) {
2047
- throw new Error("github_update_issue needs at least one of title, body, state");
2332
+ const addRelations = input.relations?.length ? input.relations : void 0;
2333
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
2334
+ const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
2335
+ if (!hasEditFields && !state) {
2336
+ throw new Error(
2337
+ "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
2338
+ );
2048
2339
  }
2049
2340
  const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
2050
2341
  if (state === "closed" || state === "close") {
@@ -2060,10 +2351,46 @@ async function updateGitHubIssue(input, opts) {
2060
2351
  } else if (state) {
2061
2352
  throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
2062
2353
  }
2063
- if (title || body !== void 0) {
2064
- const args = ["issue", "edit", String(number), ...repoArgs];
2065
- if (title) args.push("--title", title);
2066
- if (body !== void 0) args.push("--body", body);
2354
+ const args = ["issue", "edit", String(number), ...repoArgs];
2355
+ if (title) args.push("--title", title);
2356
+ if (body !== void 0) args.push("--body", body);
2357
+ if (input.labels !== void 0) {
2358
+ const existing = await getGitHubIssue(String(number), opts);
2359
+ const desired = input.labels.map((name) => name.trim()).filter(Boolean);
2360
+ const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
2361
+ const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
2362
+ if (add.length) args.push("--add-label", add.join(","));
2363
+ if (remove.length) args.push("--remove-label", remove.join(","));
2364
+ }
2365
+ if (input.project !== void 0) {
2366
+ const current = await listGitHubIssueProjects(number, cwd, repoArgs);
2367
+ if (isGitHubNoneToken(input.project)) {
2368
+ for (const name of current) args.push("--remove-project", name);
2369
+ } else {
2370
+ const wanted = (input.project ?? "").trim();
2371
+ if (!current.some((name) => labelsEqual(name, wanted))) {
2372
+ args.push("--add-project", wanted);
2373
+ }
2374
+ for (const name of current) {
2375
+ if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
2376
+ }
2377
+ }
2378
+ }
2379
+ if (input.parent !== void 0) {
2380
+ if (isGitHubNoneToken(input.parent)) {
2381
+ args.push("--remove-parent");
2382
+ } else {
2383
+ args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
2384
+ }
2385
+ }
2386
+ for (const rel of removeRelations ?? []) {
2387
+ args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
2388
+ }
2389
+ for (const rel of addRelations ?? []) {
2390
+ args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
2391
+ }
2392
+ const editChanged = args.length > 3 + repoArgs.length;
2393
+ if (editChanged) {
2067
2394
  requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
2068
2395
  }
2069
2396
  return getGitHubIssue(String(number), opts);
@@ -3659,14 +3986,23 @@ function registerAbleTimeTools(server) {
3659
3986
  }
3660
3987
  }
3661
3988
  );
3989
+ const relationInputSchema = z4.object({
3990
+ type: z4.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
3991
+ issue: z4.string().describe("Related task id or reference (CRM-232).")
3992
+ });
3662
3993
  server.tool(
3663
3994
  "abletime_update_task",
3664
- "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.",
3995
+ '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.',
3665
3996
  {
3666
3997
  id: z4.string(),
3667
3998
  title: z4.string().optional(),
3668
3999
  description: z4.string().optional(),
3669
- state: z4.string().optional()
4000
+ state: z4.string().optional(),
4001
+ labels: z4.array(z4.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
4002
+ project: z4.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
4003
+ parent: z4.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
4004
+ relations: z4.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
4005
+ removeRelations: z4.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
3670
4006
  },
3671
4007
  async (args) => {
3672
4008
  try {
@@ -3800,14 +4136,23 @@ function registerGithubIssueTools(server) {
3800
4136
  }
3801
4137
  }
3802
4138
  );
4139
+ const relationInputSchema = z5.object({
4140
+ type: z5.string().describe("blocks or blockedBy (this issue's view)."),
4141
+ issue: z5.string().describe("Related issue #123 or URL.")
4142
+ });
3803
4143
  server.tool(
3804
4144
  "github_update_issue",
3805
- "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.",
4145
+ '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.',
3806
4146
  {
3807
4147
  id: z5.string(),
3808
4148
  title: z5.string().optional(),
3809
4149
  body: z5.string().optional(),
3810
4150
  state: z5.string().optional().describe("open or closed"),
4151
+ labels: z5.array(z5.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
4152
+ project: z5.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
4153
+ parent: z5.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
4154
+ relations: z5.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
4155
+ removeRelations: z5.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
3811
4156
  repoPath: repoPathSchema
3812
4157
  },
3813
4158
  async (args) => {
@@ -3937,9 +4282,13 @@ function registerLinearTools(server) {
3937
4282
  }
3938
4283
  }
3939
4284
  );
4285
+ const relationInputSchema = z6.object({
4286
+ type: z6.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
4287
+ issue: z6.string().describe("Related issue uuid or identifier (ENG-123).")
4288
+ });
3940
4289
  server.tool(
3941
4290
  "linear_update_issue",
3942
- '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.',
4291
+ '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.',
3943
4292
  {
3944
4293
  id: z6.string(),
3945
4294
  title: z6.string().optional(),
@@ -3947,7 +4296,12 @@ function registerLinearTools(server) {
3947
4296
  state: z6.string().optional(),
3948
4297
  assignee: z6.string().nullable().optional(),
3949
4298
  priority: prioritySchema,
3950
- cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
4299
+ cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
4300
+ labels: z6.array(z6.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
4301
+ project: z6.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
4302
+ parent: z6.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
4303
+ relations: z6.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
4304
+ removeRelations: z6.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
3951
4305
  },
3952
4306
  async (args) => {
3953
4307
  try {
@@ -7985,6 +8339,8 @@ async function startSlackRelayServer(opts) {
7985
8339
  }
7986
8340
  export {
7987
8341
  ABLETIME_MCP_PATH,
8342
+ ABLETIME_MCP_PM_PATH,
8343
+ ABLETIME_PM_TOOL_NAMES,
7988
8344
  AGENT_GIT_ACTIONS,
7989
8345
  AGENT_RUNNER_MAX_OLD_SPACE_MB,
7990
8346
  ATTACHMENTS_DIR,
@@ -8074,8 +8430,10 @@ export {
8074
8430
  WORKTREE_GITHUB_MCP_TOOLS,
8075
8431
  WORKTREE_LINEAR_MCP_TOOLS,
8076
8432
  WORKTREE_MCP_TOOLS,
8433
+ ableTimeDependencyTarget,
8077
8434
  abletimeMcpRequest,
8078
8435
  abletimeMcpUrl,
8436
+ abletimeToolUsesPm,
8079
8437
  ackSlackInboundSeen,
8080
8438
  addBoardPin,
8081
8439
  addPrStackLayer,
@@ -8348,6 +8706,7 @@ export {
8348
8706
  git,
8349
8707
  githubAgentGitEnv,
8350
8708
  githubHttpsInsteadOfEntries,
8709
+ githubRelationEditFlag,
8351
8710
  globalAgentCwd,
8352
8711
  groupHomeBoardWorktrees,
8353
8712
  handleSlackInbound,
@@ -8397,6 +8756,7 @@ export {
8397
8756
  isInternalAgentStatusText,
8398
8757
  isIssueSourceConnected,
8399
8758
  isLinearConnected,
8759
+ isLinearNoneToken,
8400
8760
  isLinearOAuthCancelled,
8401
8761
  isOptionalServiceId,
8402
8762
  isOrchestratorCapableAgent,
@@ -8434,6 +8794,7 @@ export {
8434
8794
  linearGraphql,
8435
8795
  linearOAuthAuthorizeUrl,
8436
8796
  linearOAuthCredentials,
8797
+ linearRelationCreateInput,
8437
8798
  linearTicketIdFromThread,
8438
8799
  listAbleTimeAssignedIssues,
8439
8800
  listAbleTimeProjects,
@@ -8502,6 +8863,7 @@ export {
8502
8863
  nonInteractiveGitProcessEnv,
8503
8864
  normalizeAbleTimeHost,
8504
8865
  normalizeCodeSelection,
8866
+ normalizeLinearRelationType,
8505
8867
  normalizeParseResult,
8506
8868
  normalizeServiceOrigin,
8507
8869
  normalizeThinkingEffort,
@@ -8595,6 +8957,8 @@ export {
8595
8957
  resolveGithubAgentToken,
8596
8958
  resolveGithubRepoSlug,
8597
8959
  resolveLinearCycle,
8960
+ resolveLinearLabel,
8961
+ resolveLinearProject,
8598
8962
  resolveLinearState,
8599
8963
  resolveLinearTeam,
8600
8964
  resolveLoginCommand,