@sideboard-ai/core 0.1.187 → 0.1.189

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.
@@ -1610,11 +1610,45 @@ function looksLikeHugeToolResultDump(text7) {
1610
1610
  if (t.length < 4e3) return false;
1611
1611
  return looksLikeMinifiedJsDump(t);
1612
1612
  }
1613
+ function clipJobWaitResultJson(content) {
1614
+ const trimmed = content.trim();
1615
+ if (!trimmed.startsWith("{")) return void 0;
1616
+ try {
1617
+ const rec = JSON.parse(trimmed);
1618
+ const isJob = typeof rec.stillRunning === "boolean" || typeof rec.stopped === "boolean" || typeof rec.delta === "string" && typeof rec.status === "string";
1619
+ if (!isJob) return void 0;
1620
+ if (typeof rec.delta === "string" && rec.delta.length > 3500) {
1621
+ rec.delta = `\u2026(truncated ${rec.delta.length} chars)
1622
+ ${rec.delta.slice(-3500)}`;
1623
+ }
1624
+ if (typeof rec.progress === "string" && rec.progress.length > 800) {
1625
+ rec.progress = rec.progress.slice(-800);
1626
+ }
1627
+ if (typeof rec.hint === "string" && rec.hint.length > 240) {
1628
+ rec.hint = rec.hint.slice(0, 240);
1629
+ }
1630
+ let out = JSON.stringify(rec);
1631
+ if (out.length <= TOOL_RESULT_STORE_MAX_CHARS) return out;
1632
+ return JSON.stringify({
1633
+ stillRunning: rec.stillRunning,
1634
+ ok: rec.ok,
1635
+ failed: rec.failed,
1636
+ status: rec.status,
1637
+ id: rec.id,
1638
+ phase: rec.phase,
1639
+ delta: typeof rec.delta === "string" ? rec.delta.slice(-2500) : ""
1640
+ });
1641
+ } catch {
1642
+ return void 0;
1643
+ }
1644
+ }
1613
1645
  function clipToolResultForStore(content) {
1614
1646
  if (content == null) return void 0;
1615
1647
  if (looksLikeCursorSdkSourceDump(content) || looksLikeMinifiedJsDump(content)) {
1616
1648
  return `${HUGE_TOOL_RESULT_SUMMARY} (${content.length} chars omitted)`;
1617
1649
  }
1650
+ const jobClipped = clipJobWaitResultJson(content);
1651
+ if (jobClipped) return jobClipped;
1618
1652
  if (content.length <= TOOL_RESULT_STORE_MAX_CHARS) return content;
1619
1653
  const head = 5e3;
1620
1654
  const tail = 2e3;
@@ -14340,8 +14374,37 @@ function listRunningDetachedJobs(worktreePath) {
14340
14374
  }
14341
14375
  function looksLikeDeferredDonePromise(text7) {
14342
14376
  const t = (text7 ?? "").trim();
14343
- if (!t) return false;
14344
- return /\b(i['’]?ll|i will)\s+let you know\b/i.test(t) || /\blet you know when\b/i.test(t) || /\b(check back|ping me)\s+when\b/i.test(t) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(t);
14377
+ if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
14378
+ const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
14379
+ if (!unquoted) return false;
14380
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
14381
+ }
14382
+ function isJobContinuePrompt(text7) {
14383
+ const t = text7.trim();
14384
+ return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
14385
+ }
14386
+ function collapseGhRunWatchLog(text7) {
14387
+ if (!text7.includes(GH_WATCH_REFRESH)) return text7;
14388
+ const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
14389
+ const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
14390
+ if (watchFrames.length <= 1) return text7;
14391
+ const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
14392
+ const last = watchFrames[watchFrames.length - 1] ?? "";
14393
+ const omitted = watchFrames.length - 1;
14394
+ const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
14395
+ `;
14396
+ return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
14397
+ }
14398
+ function capJobLogDelta(text7) {
14399
+ const collapsed = collapseGhRunWatchLog(text7);
14400
+ const lines = collapsed.split("\n");
14401
+ let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
14402
+ ${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
14403
+ if (out.length > MAX_JOB_DELTA_CHARS) {
14404
+ out = `\u2026(truncated ${out.length} chars)
14405
+ ${out.slice(-MAX_JOB_DELTA_CHARS)}`;
14406
+ }
14407
+ return out;
14345
14408
  }
14346
14409
  function formatJobStillRunningContinuePrompt(jobIds) {
14347
14410
  const ids = jobIds.join(", ");
@@ -14404,7 +14467,7 @@ function takeDelta(logFile, cursorFile) {
14404
14467
  const lines = readLogLines(logFile);
14405
14468
  const cursor = readIntFile(cursorFile) ?? 0;
14406
14469
  const start = Math.min(Math.max(0, cursor), lines.length);
14407
- return { delta: lines.slice(start).join("\n"), nextCursor: lines.length };
14470
+ return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
14408
14471
  }
14409
14472
  function snapshotJob(dir) {
14410
14473
  const pid = readIntFile((0, import_node_path40.join)(dir, "pid"));
@@ -14572,7 +14635,7 @@ async function stopDetachedJob(cwd, id, opts) {
14572
14635
  hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
14573
14636
  };
14574
14637
  }
14575
- var import_node_fs40, import_node_path40, MAX_JOB_CONTINUES, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
14638
+ var import_node_fs40, import_node_path40, MAX_JOB_CONTINUES, MAX_JOB_DELTA_LINES, MAX_JOB_DELTA_CHARS, DEFERRED_DONE_MAX_CHARS, GH_WATCH_REFRESH, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
14576
14639
  var init_wait_for_job = __esm({
14577
14640
  "src/mcp/wait-for-job.ts"() {
14578
14641
  "use strict";
@@ -14581,6 +14644,10 @@ var init_wait_for_job = __esm({
14581
14644
  init_workspace_scratch();
14582
14645
  init_wait_for_turn();
14583
14646
  MAX_JOB_CONTINUES = 8;
14647
+ MAX_JOB_DELTA_LINES = 80;
14648
+ MAX_JOB_DELTA_CHARS = 4e3;
14649
+ DEFERRED_DONE_MAX_CHARS = 480;
14650
+ GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
14584
14651
  MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
14585
14652
  MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
14586
14653
  JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
@@ -18232,7 +18299,7 @@ function formatIssueToolsDirective(opts) {
18232
18299
  ];
18233
18300
  if (opts.linear) {
18234
18301
  lines.push(
18235
- "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
18302
+ "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
18236
18303
  );
18237
18304
  }
18238
18305
  if (github) {
@@ -20097,11 +20164,13 @@ var init_orchestrator = __esm({
20097
20164
  this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
20098
20165
  thread = this.requireThread(threadId);
20099
20166
  }
20100
- const sentAttachments = thread.attachments.length > 0 ? [...thread.attachments] : void 0;
20167
+ const autoContinue = isJobContinuePrompt(prompt) || prompt.trim().startsWith("The previous agent process ended before it finished.");
20168
+ const sentAttachments = !autoContinue && thread.attachments.length > 0 ? [...thread.attachments] : void 0;
20101
20169
  appendMessage(threadId, {
20102
20170
  role: "user",
20103
20171
  text: prompt,
20104
20172
  ...sentAttachments ? { attachments: sentAttachments } : {},
20173
+ ...autoContinue ? { origin: "continue" } : {},
20105
20174
  ts: (/* @__PURE__ */ new Date()).toISOString()
20106
20175
  });
20107
20176
  thread = this.requireThread(threadId);
@@ -20110,7 +20179,7 @@ var init_orchestrator = __esm({
20110
20179
  thread.worktreePath,
20111
20180
  gitPrompt,
20112
20181
  {
20113
- attachments: sentAttachments ?? thread.attachments
20182
+ attachments: autoContinue ? [] : sentAttachments ?? thread.attachments
20114
20183
  }
20115
20184
  );
20116
20185
  const slackInbound = /^(Slack DM|Slack @mention)(?:\n|$)/.test(
@@ -20163,7 +20232,7 @@ var init_orchestrator = __esm({
20163
20232
  slackReplyContext,
20164
20233
  expandedPrompt
20165
20234
  ].filter(Boolean).join("\n\n");
20166
- if (thread.attachments.length > 0) {
20235
+ if (!autoContinue && thread.attachments.length > 0) {
20167
20236
  updateThread(threadId, { attachments: [] });
20168
20237
  }
20169
20238
  const adapter = getAdapter(thread.agent);
@@ -21996,7 +22065,7 @@ var LIST_ISSUE_FIELDS = `
21996
22065
  assignee { id name }
21997
22066
  team { id key }
21998
22067
  labels(first: 10) { nodes { name } }
21999
- cycle { name number startsAt endsAt completedAt }
22068
+ cycle { id name number startsAt endsAt completedAt }
22000
22069
  `;
22001
22070
  var ISSUE_REF_FIELDS = `
22002
22071
  id
@@ -22022,7 +22091,7 @@ var ISSUE_FIELDS = `
22022
22091
  creator { id name }
22023
22092
  team { id key name }
22024
22093
  labels(first: 50) { nodes { name } }
22025
- cycle { name number startsAt endsAt completedAt }
22094
+ cycle { id name number startsAt endsAt completedAt }
22026
22095
  project { id name }
22027
22096
  parent { ${ISSUE_REF_FIELDS} }
22028
22097
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
@@ -22107,6 +22176,19 @@ query SideboardTeams {
22107
22176
  key
22108
22177
  name
22109
22178
  states(first: 50) { nodes { id name type } }
22179
+ activeCycle { id name number startsAt endsAt completedAt }
22180
+ }
22181
+ }
22182
+ }
22183
+ `;
22184
+ var TEAM_CYCLES_QUERY = `
22185
+ query SideboardTeamCycles($id: String!) {
22186
+ team(id: $id) {
22187
+ id
22188
+ key
22189
+ activeCycle { id name number startsAt endsAt completedAt }
22190
+ cycles(first: 40) {
22191
+ nodes { id name number startsAt endsAt completedAt }
22110
22192
  }
22111
22193
  }
22112
22194
  }
@@ -22199,11 +22281,26 @@ function linearCycleIsActive(cycle, now = Date.now()) {
22199
22281
  if (Number.isFinite(end) && now > end) return false;
22200
22282
  return true;
22201
22283
  }
22284
+ function cycleDisplayName(node) {
22285
+ return String(node?.name ?? (node?.number != null ? `Cycle ${node.number}` : "")).trim();
22286
+ }
22287
+ function mapNamedCycle(node) {
22288
+ if (!node?.id) return null;
22289
+ const name = cycleDisplayName(node);
22290
+ if (!name) return null;
22291
+ return {
22292
+ id: String(node.id),
22293
+ name,
22294
+ number: typeof node.number === "number" ? node.number : void 0,
22295
+ isActive: linearCycleIsActive(node)
22296
+ };
22297
+ }
22202
22298
  function mapCycle(node) {
22203
22299
  if (!node?.name && node?.number == null) return null;
22204
- const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
22300
+ const name = cycleDisplayName(node);
22205
22301
  if (!name) return null;
22206
22302
  return {
22303
+ ...node.id ? { id: String(node.id) } : {},
22207
22304
  name,
22208
22305
  number: typeof node.number === "number" ? node.number : void 0,
22209
22306
  isActive: linearCycleIsActive(node)
@@ -22342,6 +22439,10 @@ function toIssueInfo(issue) {
22342
22439
  };
22343
22440
  }
22344
22441
  function mapTeam(node) {
22442
+ const cycles = (node.cycles?.nodes ?? []).flatMap((c) => {
22443
+ const cycle = mapNamedCycle(c);
22444
+ return cycle ? [cycle] : [];
22445
+ });
22345
22446
  return {
22346
22447
  id: String(node.id ?? ""),
22347
22448
  key: String(node.key ?? ""),
@@ -22349,7 +22450,9 @@ function mapTeam(node) {
22349
22450
  states: (node.states?.nodes ?? []).flatMap((s) => {
22350
22451
  const state = mapState(s);
22351
22452
  return state ? [state] : [];
22352
- })
22453
+ }),
22454
+ activeCycle: mapNamedCycle(node.activeCycle),
22455
+ ...cycles.length ? { cycles } : {}
22353
22456
  };
22354
22457
  }
22355
22458
  function resolveLinearTeam(teams, team) {
@@ -22376,6 +22479,27 @@ function resolveLinearState(team, state) {
22376
22479
  }
22377
22480
  return found;
22378
22481
  }
22482
+ function resolveLinearCycle(team, cycle) {
22483
+ if (cycle == null) return null;
22484
+ const s = cycle.trim();
22485
+ if (!s || /^(none|clear|unschedule|null)$/i.test(s)) return null;
22486
+ if (/^(current|active|this)$/i.test(s)) {
22487
+ const active = team.activeCycle ?? team.cycles?.find((c) => c.isActive);
22488
+ if (!active?.id) {
22489
+ throw new Error(`Linear team ${team.key} has no active cycle`);
22490
+ }
22491
+ return active.id;
22492
+ }
22493
+ const lower = s.toLowerCase();
22494
+ const asNumber = Number.parseInt(s, 10);
22495
+ const cycles = team.cycles ?? [];
22496
+ 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);
22497
+ if (!found) {
22498
+ const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
22499
+ throw new Error(`Linear cycle not found on ${team.key}: ${cycle}. Available: ${available}`);
22500
+ }
22501
+ return found.id;
22502
+ }
22379
22503
  function normalizePriority(priority) {
22380
22504
  if (priority == null) return void 0;
22381
22505
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -22517,6 +22641,19 @@ async function listLinearTeams(opts) {
22517
22641
  teams: (json.teams?.nodes ?? []).map(mapTeam)
22518
22642
  };
22519
22643
  }
22644
+ async function listLinearTeamCycles(teamId, opts) {
22645
+ const json = await linearGraphql(TEAM_CYCLES_QUERY, { id: teamId }, opts);
22646
+ if (!json.team?.id) {
22647
+ throw new Error(`Linear team not found: ${teamId}`);
22648
+ }
22649
+ const mapped = mapTeam(json.team);
22650
+ return {
22651
+ id: mapped.id,
22652
+ key: mapped.key,
22653
+ activeCycle: mapped.activeCycle,
22654
+ cycles: mapped.cycles ?? []
22655
+ };
22656
+ }
22520
22657
  async function getLinearIssue(id, opts) {
22521
22658
  const issueId = id.trim();
22522
22659
  if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
@@ -22572,16 +22709,28 @@ async function updateLinearIssue(input, opts) {
22572
22709
  if (input.description !== void 0) {
22573
22710
  mutationInput.description = input.description;
22574
22711
  }
22712
+ const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
22713
+ const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
22575
22714
  if (input.state?.trim()) {
22576
- const existing = await getLinearIssue(issueId, opts);
22577
22715
  const { teams } = await listLinearTeams(opts);
22578
- const teamRef = existing.team?.key || existing.team?.id;
22716
+ const teamRef = existing?.team?.key || existing?.team?.id;
22579
22717
  const team = teamRef ? resolveLinearTeam(teams, teamRef) : void 0;
22580
22718
  if (!team?.states.length) {
22581
- throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
22719
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no workflow states to resolve "${input.state}"`);
22582
22720
  }
22583
22721
  mutationInput.stateId = resolveLinearState(team, input.state).id;
22584
22722
  }
22723
+ if (input.cycle !== void 0) {
22724
+ const teamId = existing?.team?.id;
22725
+ if (!teamId) {
22726
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no team to resolve a cycle`);
22727
+ }
22728
+ const cycles = await listLinearTeamCycles(teamId, opts);
22729
+ mutationInput.cycleId = resolveLinearCycle(
22730
+ { ...cycles, key: cycles.key || existing?.team?.key || teamId },
22731
+ input.cycle
22732
+ );
22733
+ }
22585
22734
  if (input.assignee !== void 0) {
22586
22735
  const assignee = input.assignee?.trim() || null;
22587
22736
  if (assignee === "me") {
@@ -22595,7 +22744,7 @@ async function updateLinearIssue(input, opts) {
22595
22744
  mutationInput.priority = normalizePriority(input.priority);
22596
22745
  }
22597
22746
  if (Object.keys(mutationInput).length === 0) {
22598
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
22747
+ throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
22599
22748
  }
22600
22749
  const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
22601
22750
  if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
@@ -24063,7 +24212,7 @@ var prioritySchema = import_zod6.z.number().int().min(0).max(4).optional().descr
24063
24212
  function registerLinearTools(server) {
24064
24213
  server.tool(
24065
24214
  "linear_list_teams",
24066
- "List Linear teams (id, key, name) and workflow states. Use team key on create; state name or type on create/update.",
24215
+ 'List Linear teams (id, key, name), workflow states, and the active cycle. Use team key on create; state name or type on create/update; cycle name/"current"/"none" on update.',
24067
24216
  {},
24068
24217
  async () => {
24069
24218
  try {
@@ -24150,14 +24299,15 @@ function registerLinearTools(server) {
24150
24299
  );
24151
24300
  server.tool(
24152
24301
  "linear_update_issue",
24153
- "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority. 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.",
24302
+ '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.',
24154
24303
  {
24155
24304
  id: import_zod6.z.string(),
24156
24305
  title: import_zod6.z.string().optional(),
24157
24306
  description: import_zod6.z.string().optional(),
24158
24307
  state: import_zod6.z.string().optional(),
24159
24308
  assignee: import_zod6.z.string().nullable().optional(),
24160
- priority: prioritySchema
24309
+ priority: prioritySchema,
24310
+ cycle: import_zod6.z.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
24161
24311
  },
24162
24312
  async (args) => {
24163
24313
  try {
@@ -37,7 +37,7 @@ import {
37
37
  syncBoardPins,
38
38
  updateSchedule,
39
39
  waitForDetachedJob
40
- } from "../chunk-S6HYHXFO.js";
40
+ } from "../chunk-LE5OZCKB.js";
41
41
  import {
42
42
  applySystemCaEnv,
43
43
  isCursorApiKeyConfigured,
@@ -46,7 +46,7 @@ import {
46
46
  materializeSystemCaBundle,
47
47
  sideboardMcpProfile,
48
48
  trustSystemCertificates
49
- } from "../chunk-7DBYKEYF.js";
49
+ } from "../chunk-QU663NPO.js";
50
50
  import "../chunk-HVIEYTUK.js";
51
51
  import "../chunk-Y4MIXGPA.js";
52
52
  import {
@@ -531,7 +531,7 @@ var LIST_ISSUE_FIELDS = `
531
531
  assignee { id name }
532
532
  team { id key }
533
533
  labels(first: 10) { nodes { name } }
534
- cycle { name number startsAt endsAt completedAt }
534
+ cycle { id name number startsAt endsAt completedAt }
535
535
  `;
536
536
  var ISSUE_REF_FIELDS = `
537
537
  id
@@ -557,7 +557,7 @@ var ISSUE_FIELDS = `
557
557
  creator { id name }
558
558
  team { id key name }
559
559
  labels(first: 50) { nodes { name } }
560
- cycle { name number startsAt endsAt completedAt }
560
+ cycle { id name number startsAt endsAt completedAt }
561
561
  project { id name }
562
562
  parent { ${ISSUE_REF_FIELDS} }
563
563
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
@@ -642,6 +642,19 @@ query SideboardTeams {
642
642
  key
643
643
  name
644
644
  states(first: 50) { nodes { id name type } }
645
+ activeCycle { id name number startsAt endsAt completedAt }
646
+ }
647
+ }
648
+ }
649
+ `;
650
+ var TEAM_CYCLES_QUERY = `
651
+ query SideboardTeamCycles($id: String!) {
652
+ team(id: $id) {
653
+ id
654
+ key
655
+ activeCycle { id name number startsAt endsAt completedAt }
656
+ cycles(first: 40) {
657
+ nodes { id name number startsAt endsAt completedAt }
645
658
  }
646
659
  }
647
660
  }
@@ -734,11 +747,26 @@ function linearCycleIsActive(cycle, now = Date.now()) {
734
747
  if (Number.isFinite(end) && now > end) return false;
735
748
  return true;
736
749
  }
750
+ function cycleDisplayName(node) {
751
+ return String(node?.name ?? (node?.number != null ? `Cycle ${node.number}` : "")).trim();
752
+ }
753
+ function mapNamedCycle(node) {
754
+ if (!node?.id) return null;
755
+ const name = cycleDisplayName(node);
756
+ if (!name) return null;
757
+ return {
758
+ id: String(node.id),
759
+ name,
760
+ number: typeof node.number === "number" ? node.number : void 0,
761
+ isActive: linearCycleIsActive(node)
762
+ };
763
+ }
737
764
  function mapCycle(node) {
738
765
  if (!node?.name && node?.number == null) return null;
739
- const name = String(node.name ?? (node.number != null ? `Cycle ${node.number}` : "")).trim();
766
+ const name = cycleDisplayName(node);
740
767
  if (!name) return null;
741
768
  return {
769
+ ...node.id ? { id: String(node.id) } : {},
742
770
  name,
743
771
  number: typeof node.number === "number" ? node.number : void 0,
744
772
  isActive: linearCycleIsActive(node)
@@ -877,6 +905,10 @@ function toIssueInfo(issue) {
877
905
  };
878
906
  }
879
907
  function mapTeam(node) {
908
+ const cycles = (node.cycles?.nodes ?? []).flatMap((c) => {
909
+ const cycle = mapNamedCycle(c);
910
+ return cycle ? [cycle] : [];
911
+ });
880
912
  return {
881
913
  id: String(node.id ?? ""),
882
914
  key: String(node.key ?? ""),
@@ -884,7 +916,9 @@ function mapTeam(node) {
884
916
  states: (node.states?.nodes ?? []).flatMap((s) => {
885
917
  const state = mapState(s);
886
918
  return state ? [state] : [];
887
- })
919
+ }),
920
+ activeCycle: mapNamedCycle(node.activeCycle),
921
+ ...cycles.length ? { cycles } : {}
888
922
  };
889
923
  }
890
924
  function resolveLinearTeam(teams, team) {
@@ -911,6 +945,27 @@ function resolveLinearState(team, state) {
911
945
  }
912
946
  return found;
913
947
  }
948
+ function resolveLinearCycle(team, cycle) {
949
+ if (cycle == null) return null;
950
+ const s = cycle.trim();
951
+ if (!s || /^(none|clear|unschedule|null)$/i.test(s)) return null;
952
+ if (/^(current|active|this)$/i.test(s)) {
953
+ const active = team.activeCycle ?? team.cycles?.find((c) => c.isActive);
954
+ if (!active?.id) {
955
+ throw new Error(`Linear team ${team.key} has no active cycle`);
956
+ }
957
+ return active.id;
958
+ }
959
+ const lower = s.toLowerCase();
960
+ const asNumber = Number.parseInt(s, 10);
961
+ const cycles = team.cycles ?? [];
962
+ 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);
963
+ if (!found) {
964
+ const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
965
+ throw new Error(`Linear cycle not found on ${team.key}: ${cycle}. Available: ${available}`);
966
+ }
967
+ return found.id;
968
+ }
914
969
  function normalizePriority(priority) {
915
970
  if (priority == null) return void 0;
916
971
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -1052,6 +1107,19 @@ async function listLinearTeams(opts) {
1052
1107
  teams: (json.teams?.nodes ?? []).map(mapTeam)
1053
1108
  };
1054
1109
  }
1110
+ async function listLinearTeamCycles(teamId, opts) {
1111
+ const json = await linearGraphql(TEAM_CYCLES_QUERY, { id: teamId }, opts);
1112
+ if (!json.team?.id) {
1113
+ throw new Error(`Linear team not found: ${teamId}`);
1114
+ }
1115
+ const mapped = mapTeam(json.team);
1116
+ return {
1117
+ id: mapped.id,
1118
+ key: mapped.key,
1119
+ activeCycle: mapped.activeCycle,
1120
+ cycles: mapped.cycles ?? []
1121
+ };
1122
+ }
1055
1123
  async function getLinearIssue(id, opts) {
1056
1124
  const issueId = id.trim();
1057
1125
  if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
@@ -1107,16 +1175,28 @@ async function updateLinearIssue(input, opts) {
1107
1175
  if (input.description !== void 0) {
1108
1176
  mutationInput.description = input.description;
1109
1177
  }
1178
+ const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
1179
+ const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
1110
1180
  if (input.state?.trim()) {
1111
- const existing = await getLinearIssue(issueId, opts);
1112
1181
  const { teams } = await listLinearTeams(opts);
1113
- const teamRef = existing.team?.key || existing.team?.id;
1182
+ const teamRef = existing?.team?.key || existing?.team?.id;
1114
1183
  const team = teamRef ? resolveLinearTeam(teams, teamRef) : void 0;
1115
1184
  if (!team?.states.length) {
1116
- throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
1185
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no workflow states to resolve "${input.state}"`);
1117
1186
  }
1118
1187
  mutationInput.stateId = resolveLinearState(team, input.state).id;
1119
1188
  }
1189
+ if (input.cycle !== void 0) {
1190
+ const teamId = existing?.team?.id;
1191
+ if (!teamId) {
1192
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no team to resolve a cycle`);
1193
+ }
1194
+ const cycles = await listLinearTeamCycles(teamId, opts);
1195
+ mutationInput.cycleId = resolveLinearCycle(
1196
+ { ...cycles, key: cycles.key || existing?.team?.key || teamId },
1197
+ input.cycle
1198
+ );
1199
+ }
1120
1200
  if (input.assignee !== void 0) {
1121
1201
  const assignee = input.assignee?.trim() || null;
1122
1202
  if (assignee === "me") {
@@ -1130,7 +1210,7 @@ async function updateLinearIssue(input, opts) {
1130
1210
  mutationInput.priority = normalizePriority(input.priority);
1131
1211
  }
1132
1212
  if (Object.keys(mutationInput).length === 0) {
1133
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
1213
+ throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
1134
1214
  }
1135
1215
  const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1136
1216
  if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
@@ -2573,7 +2653,7 @@ var prioritySchema = z6.number().int().min(0).max(4).optional().describe("0 none
2573
2653
  function registerLinearTools(server) {
2574
2654
  server.tool(
2575
2655
  "linear_list_teams",
2576
- "List Linear teams (id, key, name) and workflow states. Use team key on create; state name or type on create/update.",
2656
+ 'List Linear teams (id, key, name), workflow states, and the active cycle. Use team key on create; state name or type on create/update; cycle name/"current"/"none" on update.',
2577
2657
  {},
2578
2658
  async () => {
2579
2659
  try {
@@ -2660,14 +2740,15 @@ function registerLinearTools(server) {
2660
2740
  );
2661
2741
  server.tool(
2662
2742
  "linear_update_issue",
2663
- "Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, and/or priority. 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.",
2743
+ '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.',
2664
2744
  {
2665
2745
  id: z6.string(),
2666
2746
  title: z6.string().optional(),
2667
2747
  description: z6.string().optional(),
2668
2748
  state: z6.string().optional(),
2669
2749
  assignee: z6.string().nullable().optional(),
2670
- priority: prioritySchema
2750
+ priority: prioritySchema,
2751
+ cycle: z6.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
2671
2752
  },
2672
2753
  async (args) => {
2673
2754
  try {
@@ -6,9 +6,9 @@ import {
6
6
  resolveSendFollowUp,
7
7
  startOrchestration,
8
8
  waitForPidExit
9
- } from "./chunk-P56XKPWT.js";
9
+ } from "./chunk-U6FCBOS7.js";
10
10
  import "./chunk-DJU54RYW.js";
11
- import "./chunk-6XOSOIDJ.js";
11
+ import "./chunk-ISOCLQEX.js";
12
12
  import "./chunk-VIODP5PT.js";
13
13
  import "./chunk-WXKXSZN4.js";
14
14
  import "./chunk-6K64FJDD.js";
@@ -16,7 +16,7 @@ import "./chunk-IEEAVWAP.js";
16
16
  import "./chunk-AN3THU7F.js";
17
17
  import "./chunk-N7WZ6NIN.js";
18
18
  import "./chunk-AXQ4ZWHK.js";
19
- import "./chunk-NQTKRAUF.js";
19
+ import "./chunk-ZSFQLR7Y.js";
20
20
  import "./chunk-J7LQVLFQ.js";
21
21
  import "./chunk-4TR3HZFT.js";
22
22
  import "./chunk-AO35MGOE.js";
@@ -8,8 +8,8 @@ import {
8
8
  resolveSendFollowUp,
9
9
  startOrchestration,
10
10
  waitForPidExit
11
- } from "./chunk-S6HYHXFO.js";
12
- import "./chunk-7DBYKEYF.js";
11
+ } from "./chunk-LE5OZCKB.js";
12
+ import "./chunk-QU663NPO.js";
13
13
  import "./chunk-HVIEYTUK.js";
14
14
  import "./chunk-Y4MIXGPA.js";
15
15
  import "./chunk-VKQGAWHY.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.187",
3
+ "version": "0.1.189",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",