@sideboard-ai/core 0.1.190 → 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.
@@ -37,7 +37,7 @@ import {
37
37
  syncBoardPins,
38
38
  updateSchedule,
39
39
  waitForDetachedJob
40
- } from "../chunk-IAOXW6RI.js";
40
+ } from "../chunk-GPSIZGEZ.js";
41
41
  import {
42
42
  applySystemCaEnv,
43
43
  isCursorApiKeyConfigured,
@@ -60,7 +60,7 @@ import {
60
60
  searchAbleTimeTasks,
61
61
  toAbleTimeIssueInfo,
62
62
  updateAbleTimeTask
63
- } from "../chunk-KEVY4GEK.js";
63
+ } from "../chunk-UBQIELFS.js";
64
64
  import {
65
65
  httpFetch
66
66
  } from "../chunk-VKQGAWHY.js";
@@ -366,13 +366,78 @@ async function commentGitHubIssue(input, opts) {
366
366
  const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
367
367
  return { body, url };
368
368
  }
369
+ function isGitHubNoneToken(value) {
370
+ if (value == null) return true;
371
+ const s = value.trim();
372
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
373
+ }
374
+ function githubRelationEditFlag(type, remove = false) {
375
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
376
+ if (s === "blockedby" || s === "blocked" || s === "dependson") {
377
+ return remove ? "--remove-blocked-by" : "--add-blocked-by";
378
+ }
379
+ if (s === "blocks" || s === "blocking") {
380
+ return remove ? "--remove-blocking" : "--add-blocking";
381
+ }
382
+ throw new Error(
383
+ `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
384
+ );
385
+ }
386
+ function labelsEqual(a, b) {
387
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
388
+ }
389
+ function parseProjectTitles(raw) {
390
+ const out = [];
391
+ const seen = /* @__PURE__ */ new Set();
392
+ const push = (name) => {
393
+ const title = name.trim();
394
+ const key = title.toLowerCase();
395
+ if (!title || seen.has(key)) return;
396
+ seen.add(key);
397
+ out.push(title);
398
+ };
399
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
400
+ if (Array.isArray(fromItems)) {
401
+ for (const item of fromItems) {
402
+ if (typeof item === "string") {
403
+ push(item);
404
+ continue;
405
+ }
406
+ if (!item || typeof item !== "object") continue;
407
+ const rec = item;
408
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
409
+ const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
410
+ if (title) push(title);
411
+ }
412
+ }
413
+ return out;
414
+ }
415
+ async function listGitHubIssueProjects(number, cwd, repoArgs) {
416
+ const result = await gh(
417
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems,projects"],
418
+ cwd,
419
+ { reject: false }
420
+ );
421
+ if (result.exitCode !== 0 || !result.stdout.trim()) return [];
422
+ try {
423
+ const parsed = JSON.parse(result.stdout);
424
+ return parseProjectTitles(parsed);
425
+ } catch {
426
+ return [];
427
+ }
428
+ }
369
429
  async function updateGitHubIssue(input, opts) {
370
430
  const number = parseGitHubIssueNumber(input.id);
371
431
  const title = input.title?.trim();
372
432
  const body = input.body;
373
433
  const state = input.state?.trim().toLowerCase();
374
- if (!title && body === void 0 && !state) {
375
- throw new Error("github_update_issue needs at least one of title, body, state");
434
+ const addRelations = input.relations?.length ? input.relations : void 0;
435
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
436
+ const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
437
+ if (!hasEditFields && !state) {
438
+ throw new Error(
439
+ "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
440
+ );
376
441
  }
377
442
  const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
378
443
  if (state === "closed" || state === "close") {
@@ -388,10 +453,46 @@ async function updateGitHubIssue(input, opts) {
388
453
  } else if (state) {
389
454
  throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
390
455
  }
391
- if (title || body !== void 0) {
392
- const args = ["issue", "edit", String(number), ...repoArgs];
393
- if (title) args.push("--title", title);
394
- if (body !== void 0) args.push("--body", body);
456
+ const args = ["issue", "edit", String(number), ...repoArgs];
457
+ if (title) args.push("--title", title);
458
+ if (body !== void 0) args.push("--body", body);
459
+ if (input.labels !== void 0) {
460
+ const existing = await getGitHubIssue(String(number), opts);
461
+ const desired = input.labels.map((name) => name.trim()).filter(Boolean);
462
+ const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
463
+ const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
464
+ if (add.length) args.push("--add-label", add.join(","));
465
+ if (remove.length) args.push("--remove-label", remove.join(","));
466
+ }
467
+ if (input.project !== void 0) {
468
+ const current = await listGitHubIssueProjects(number, cwd, repoArgs);
469
+ if (isGitHubNoneToken(input.project)) {
470
+ for (const name of current) args.push("--remove-project", name);
471
+ } else {
472
+ const wanted = (input.project ?? "").trim();
473
+ if (!current.some((name) => labelsEqual(name, wanted))) {
474
+ args.push("--add-project", wanted);
475
+ }
476
+ for (const name of current) {
477
+ if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
478
+ }
479
+ }
480
+ }
481
+ if (input.parent !== void 0) {
482
+ if (isGitHubNoneToken(input.parent)) {
483
+ args.push("--remove-parent");
484
+ } else {
485
+ args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
486
+ }
487
+ }
488
+ for (const rel of removeRelations ?? []) {
489
+ args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
490
+ }
491
+ for (const rel of addRelations ?? []) {
492
+ args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
493
+ }
494
+ const editChanged = args.length > 3 + repoArgs.length;
495
+ if (editChanged) {
395
496
  requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
396
497
  }
397
498
  return getGitHubIssue(String(number), opts);
@@ -563,12 +664,14 @@ var ISSUE_FIELDS = `
563
664
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
564
665
  relations(first: 25) {
565
666
  nodes {
667
+ id
566
668
  type
567
669
  relatedIssue { ${ISSUE_REF_FIELDS} }
568
670
  }
569
671
  }
570
672
  inverseRelations(first: 25) {
571
673
  nodes {
674
+ id
572
675
  type
573
676
  issue { ${ISSUE_REF_FIELDS} }
574
677
  }
@@ -688,6 +791,35 @@ mutation SideboardCommentCreate($input: CommentCreateInput!) {
688
791
  }
689
792
  }
690
793
  `;
794
+ var LABELS_QUERY = `
795
+ query SideboardLabels($first: Int!) {
796
+ issueLabels(first: $first) {
797
+ nodes { id name }
798
+ }
799
+ }
800
+ `;
801
+ var PROJECTS_QUERY = `
802
+ query SideboardProjects($first: Int!, $filter: ProjectFilter) {
803
+ projects(first: $first, filter: $filter) {
804
+ nodes { id name slugId }
805
+ }
806
+ }
807
+ `;
808
+ var ISSUE_RELATION_CREATE = `
809
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
810
+ issueRelationCreate(input: $input) {
811
+ success
812
+ issueRelation { id type }
813
+ }
814
+ }
815
+ `;
816
+ var ISSUE_RELATION_DELETE = `
817
+ mutation SideboardIssueRelationDelete($id: String!) {
818
+ issueRelationDelete(id: $id) {
819
+ success
820
+ }
821
+ }
822
+ `;
691
823
  function rewriteLinearError(message) {
692
824
  if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
693
825
  return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
@@ -807,12 +939,18 @@ function mapRelations(node) {
807
939
  for (const rel of node.relations?.nodes ?? []) {
808
940
  const issue = mapIssueRef(rel.relatedIssue);
809
941
  if (!issue) continue;
810
- out.push({ type: String(rel.type ?? "related"), issue });
942
+ const id = rel.id?.trim();
943
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
811
944
  }
812
945
  for (const rel of node.inverseRelations?.nodes ?? []) {
813
946
  const issue = mapIssueRef(rel.issue);
814
947
  if (!issue) continue;
815
- out.push({ type: flipLinearRelationType(String(rel.type ?? "related")), issue });
948
+ const id = rel.id?.trim();
949
+ out.push({
950
+ type: flipLinearRelationType(String(rel.type ?? "related")),
951
+ issue,
952
+ ...id ? { id } : {}
953
+ });
816
954
  }
817
955
  return out;
818
956
  }
@@ -973,6 +1111,148 @@ function resolveLinearCycle(team, cycle) {
973
1111
  }
974
1112
  return found.id;
975
1113
  }
1114
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
1115
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1116
+ function isLinearNoneToken(value) {
1117
+ if (value == null) return true;
1118
+ const s = value.trim();
1119
+ return !s || LINEAR_NONE_RE.test(s);
1120
+ }
1121
+ function resolveLinearLabel(labels, name) {
1122
+ const s = name.trim();
1123
+ if (!s) throw new Error("Linear label is empty");
1124
+ const lower = s.toLowerCase();
1125
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
1126
+ if (!found) {
1127
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1128
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
1129
+ }
1130
+ return found;
1131
+ }
1132
+ function resolveLinearProject(projects, project) {
1133
+ if (isLinearNoneToken(project)) return null;
1134
+ const s = (project ?? "").trim();
1135
+ const lower = s.toLowerCase();
1136
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
1137
+ if (!found) {
1138
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
1139
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
1140
+ }
1141
+ return found.id;
1142
+ }
1143
+ function normalizeLinearRelationType(type) {
1144
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
1145
+ if (s === "blocks" || s === "block") return "blocks";
1146
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
1147
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
1148
+ if (s === "duplicate" || s === "dup") return "duplicate";
1149
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
1150
+ if (s === "similar") return "similar";
1151
+ throw new Error(
1152
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
1153
+ );
1154
+ }
1155
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
1156
+ const view = normalizeLinearRelationType(type);
1157
+ if (view === "blockedBy") {
1158
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
1159
+ }
1160
+ if (view === "duplicateOf") {
1161
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
1162
+ }
1163
+ return { issueId: thisIssueId, relatedIssueId, type: view };
1164
+ }
1165
+ function relationMatches(rel, type, issueRef) {
1166
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
1167
+ const ref = issueRef.trim().toLowerCase();
1168
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
1169
+ }
1170
+ function formatRelationList(relations) {
1171
+ if (!relations.length) return "(none)";
1172
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
1173
+ }
1174
+ async function listLinearLabels(opts) {
1175
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
1176
+ return (json.issueLabels?.nodes ?? []).flatMap((node) => {
1177
+ const id = String(node.id ?? "").trim();
1178
+ const name = String(node.name ?? "").trim();
1179
+ if (!id) return [];
1180
+ return [{ id, name }];
1181
+ });
1182
+ }
1183
+ async function listLinearProjects(filter, opts) {
1184
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
1185
+ return (json.projects?.nodes ?? []).flatMap((node) => {
1186
+ const id = String(node.id ?? "").trim();
1187
+ if (!id) return [];
1188
+ return [
1189
+ {
1190
+ id,
1191
+ name: String(node.name ?? ""),
1192
+ ...node.slugId ? { slugId: String(node.slugId) } : {}
1193
+ }
1194
+ ];
1195
+ });
1196
+ }
1197
+ async function resolveLinearProjectId(project, opts) {
1198
+ if (isLinearNoneToken(project)) return null;
1199
+ const s = (project ?? "").trim();
1200
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
1201
+ const matched = await listLinearProjects(filter, opts);
1202
+ try {
1203
+ return resolveLinearProject(matched, s);
1204
+ } catch {
1205
+ if (UUID_RE.test(s)) return s;
1206
+ const listed = await listLinearProjects(void 0, opts);
1207
+ return resolveLinearProject(listed, s);
1208
+ }
1209
+ }
1210
+ async function resolveLinearLabelIds(names, opts) {
1211
+ if (names.length === 0) return [];
1212
+ const labels = await listLinearLabels(opts);
1213
+ return names.map((name) => {
1214
+ const s = name.trim();
1215
+ try {
1216
+ return resolveLinearLabel(labels, s).id;
1217
+ } catch (error) {
1218
+ if (UUID_RE.test(s)) return s;
1219
+ throw error;
1220
+ }
1221
+ });
1222
+ }
1223
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
1224
+ const current = [...issue.relations];
1225
+ for (const rel of remove ?? []) {
1226
+ const type = normalizeLinearRelationType(rel.type);
1227
+ const issueRef = rel.issue.trim();
1228
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
1229
+ const match = current.find((r) => relationMatches(r, type, issueRef));
1230
+ if (!match?.id) {
1231
+ throw new Error(
1232
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
1233
+ );
1234
+ }
1235
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
1236
+ if (!json.issueRelationDelete?.success) {
1237
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
1238
+ }
1239
+ const idx = current.indexOf(match);
1240
+ if (idx >= 0) current.splice(idx, 1);
1241
+ }
1242
+ for (const rel of add ?? []) {
1243
+ const type = normalizeLinearRelationType(rel.type);
1244
+ const issueRef = rel.issue.trim();
1245
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
1246
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
1247
+ const related = await getLinearIssue(issueRef, opts);
1248
+ const input = linearRelationCreateInput(issue.id, type, related.id);
1249
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
1250
+ if (!json.issueRelationCreate?.success) {
1251
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
1252
+ }
1253
+ current.push({ type, issue: related, id: void 0 });
1254
+ }
1255
+ }
976
1256
  function normalizePriority(priority) {
977
1257
  if (priority == null) return void 0;
978
1258
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -980,7 +1260,6 @@ function normalizePriority(priority) {
980
1260
  }
981
1261
  return priority;
982
1262
  }
983
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
984
1263
  function assigneeFilterKey(assignee) {
985
1264
  return (assignee ?? "").trim();
986
1265
  }
@@ -1187,8 +1466,10 @@ async function updateLinearIssue(input, opts) {
1187
1466
  if (input.description !== void 0) {
1188
1467
  mutationInput.description = input.description;
1189
1468
  }
1190
- const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
1191
- const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
1469
+ const addRelations = input.relations?.length ? input.relations : void 0;
1470
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
1471
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
1472
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
1192
1473
  if (input.state?.trim()) {
1193
1474
  const { teams } = await listLinearTeams(opts);
1194
1475
  const teamRef = existing?.team?.key || existing?.team?.id;
@@ -1221,14 +1502,44 @@ async function updateLinearIssue(input, opts) {
1221
1502
  if (input.priority !== void 0) {
1222
1503
  mutationInput.priority = normalizePriority(input.priority);
1223
1504
  }
1224
- if (Object.keys(mutationInput).length === 0) {
1225
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
1505
+ if (input.labels !== void 0) {
1506
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, opts);
1226
1507
  }
1227
- const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1228
- if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1229
- throw new Error("Linear issueUpdate failed");
1508
+ if (input.project !== void 0) {
1509
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
1230
1510
  }
1231
- return mapIssue(json.issueUpdate.issue);
1511
+ if (input.parent !== void 0) {
1512
+ if (isLinearNoneToken(input.parent)) {
1513
+ mutationInput.parentId = null;
1514
+ } else {
1515
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
1516
+ mutationInput.parentId = parent.id;
1517
+ }
1518
+ }
1519
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
1520
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
1521
+ if (!hasIssueFields && !hasRelationWrites) {
1522
+ throw new Error(
1523
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
1524
+ );
1525
+ }
1526
+ let updated;
1527
+ if (hasIssueFields) {
1528
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1529
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1530
+ throw new Error("Linear issueUpdate failed");
1531
+ }
1532
+ updated = mapIssue(json.issueUpdate.issue);
1533
+ }
1534
+ if (hasRelationWrites) {
1535
+ const base = updated ?? existing;
1536
+ if (!base) {
1537
+ throw new Error(`Linear issue not found: ${issueId}`);
1538
+ }
1539
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
1540
+ return getLinearIssue(base.id || issueId, opts);
1541
+ }
1542
+ return updated;
1232
1543
  }
1233
1544
  async function commentLinearIssue(input, opts) {
1234
1545
  const issueId = input.id.trim();
@@ -2472,14 +2783,23 @@ function registerAbleTimeTools(server) {
2472
2783
  }
2473
2784
  }
2474
2785
  );
2786
+ const relationInputSchema = z4.object({
2787
+ type: z4.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
2788
+ issue: z4.string().describe("Related task id or reference (CRM-232).")
2789
+ });
2475
2790
  server.tool(
2476
2791
  "abletime_update_task",
2477
- "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.",
2792
+ '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.',
2478
2793
  {
2479
2794
  id: z4.string(),
2480
2795
  title: z4.string().optional(),
2481
2796
  description: z4.string().optional(),
2482
- state: z4.string().optional()
2797
+ state: z4.string().optional(),
2798
+ labels: z4.array(z4.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
2799
+ project: z4.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
2800
+ parent: z4.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
2801
+ relations: z4.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
2802
+ removeRelations: z4.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
2483
2803
  },
2484
2804
  async (args) => {
2485
2805
  try {
@@ -2613,14 +2933,23 @@ function registerGithubIssueTools(server) {
2613
2933
  }
2614
2934
  }
2615
2935
  );
2936
+ const relationInputSchema = z5.object({
2937
+ type: z5.string().describe("blocks or blockedBy (this issue's view)."),
2938
+ issue: z5.string().describe("Related issue #123 or URL.")
2939
+ });
2616
2940
  server.tool(
2617
2941
  "github_update_issue",
2618
- "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.",
2942
+ '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.',
2619
2943
  {
2620
2944
  id: z5.string(),
2621
2945
  title: z5.string().optional(),
2622
2946
  body: z5.string().optional(),
2623
2947
  state: z5.string().optional().describe("open or closed"),
2948
+ labels: z5.array(z5.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
2949
+ project: z5.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
2950
+ parent: z5.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
2951
+ relations: z5.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
2952
+ removeRelations: z5.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
2624
2953
  repoPath: repoPathSchema
2625
2954
  },
2626
2955
  async (args) => {
@@ -2750,9 +3079,13 @@ function registerLinearTools(server) {
2750
3079
  }
2751
3080
  }
2752
3081
  );
3082
+ const relationInputSchema = z6.object({
3083
+ type: z6.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
3084
+ issue: z6.string().describe("Related issue uuid or identifier (ENG-123).")
3085
+ });
2753
3086
  server.tool(
2754
3087
  "linear_update_issue",
2755
- '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.',
3088
+ '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.',
2756
3089
  {
2757
3090
  id: z6.string(),
2758
3091
  title: z6.string().optional(),
@@ -2760,7 +3093,12 @@ function registerLinearTools(server) {
2760
3093
  state: z6.string().optional(),
2761
3094
  assignee: z6.string().nullable().optional(),
2762
3095
  priority: prioritySchema,
2763
- cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
3096
+ cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
3097
+ labels: z6.array(z6.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
3098
+ project: z6.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
3099
+ parent: z6.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
3100
+ relations: z6.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
3101
+ removeRelations: z6.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
2764
3102
  },
2765
3103
  async (args) => {
2766
3104
  try {
@@ -8,7 +8,7 @@ import {
8
8
  resolveSendFollowUp,
9
9
  startOrchestration,
10
10
  waitForPidExit
11
- } from "./chunk-IAOXW6RI.js";
11
+ } from "./chunk-GPSIZGEZ.js";
12
12
  import "./chunk-GBWKMWUQ.js";
13
13
  import "./chunk-HVIEYTUK.js";
14
14
  import "./chunk-Y4MIXGPA.js";
@@ -6,7 +6,7 @@ import {
6
6
  resolveSendFollowUp,
7
7
  startOrchestration,
8
8
  waitForPidExit
9
- } from "./chunk-Z6TW3A5P.js";
9
+ } from "./chunk-5RRAKGPK.js";
10
10
  import "./chunk-DJU54RYW.js";
11
11
  import "./chunk-OLDZXO53.js";
12
12
  import "./chunk-VIODP5PT.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.190",
3
+ "version": "0.1.191",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",