@starlove/orbit 1.1.0 → 1.1.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "edition": "full",
5
- "builtAt": "2026-05-26T04:59:52.730Z"
5
+ "builtAt": "2026-05-26T05:55:12.573Z"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Starscape Orbit ??? zero-dependency, pretty, human-ai-first kanban system.",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",
package/src/core/agent.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  relationRows,
8
8
  stateByName,
9
9
  stateByRole,
10
+ stateById,
10
11
  ticketById,
11
12
  ticketByNumber,
12
13
  ticketByTitle,
@@ -209,8 +210,11 @@ export function completeTicket(body, ctx) {
209
210
  return getContextPack(ticket.id, ctx, 1);
210
211
  }
211
212
 
212
- export function getContextPack(ticketId, ctx, depth = 1) {
213
+ export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
213
214
  const { db, board, actor } = ctx;
215
+ const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
216
+ const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
217
+ const includeRelatedFull = Boolean(options.include_related_full ?? options.includeRelatedFull);
214
218
  const ticket = ticketById(db, ticketId);
215
219
  if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
216
220
  const innerBoard = boardById(db, ticket.board_id);
@@ -233,19 +237,122 @@ export function getContextPack(ticketId, ctx, depth = 1) {
233
237
  ]);
234
238
 
235
239
  return {
236
- ticket: {
240
+ ticket: capTicketFields({
237
241
  ...ticket,
238
242
  labels
239
- },
243
+ }, maxCharsPerField),
240
244
  board: innerBoard,
241
245
  board_manual: boardManual(innerBoard.id, ctx),
242
- parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
246
+ parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
243
247
  child_tickets: childTickets.map((child) => ({
244
- ...child,
248
+ ...capTicketFields(child, maxCharsPerField),
245
249
  child_count: childTicketsFor(db, child.id).length
246
250
  })),
247
- relations,
251
+ relations: relations.map((relation) => ({
252
+ ...relation,
253
+ other_ticket: includeRelatedFull
254
+ ? capTicketFields({ ...relation.other_ticket, labels: labelsForTicket(db, relation.other_ticket.id) }, maxCharsPerField)
255
+ : compactTicket(relation.other_ticket)
256
+ })),
248
257
  blockers,
249
- related_tickets: relatedTickets
258
+ related_tickets: relatedTickets.map((related) =>
259
+ includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
260
+ )
261
+ };
262
+ }
263
+
264
+ export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
265
+ const { db, board, actor } = ctx;
266
+ const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 4000);
267
+ const commentLimit = Math.min(nonNegativeInt(options.comment_limit ?? options.commentLimit, 5), 20);
268
+ const ticket = ticketById(db, ticketId);
269
+ if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
270
+ const innerBoard = boardById(db, ticket.board_id);
271
+ requireBoardAccess(actor, innerBoard);
272
+
273
+ const labels = labelsForTicket(db, ticket.id).map((label) => label.name);
274
+ const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
275
+ const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
276
+ const comments = db
277
+ .prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT ?")
278
+ .all(ticket.id, commentLimit)
279
+ .reverse()
280
+ .map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
281
+
282
+ return {
283
+ board: {
284
+ id: innerBoard.id,
285
+ slug: innerBoard.slug,
286
+ name: innerBoard.name,
287
+ repo_path: innerBoard.repo_path || innerBoard.system_path || "",
288
+ system_path: innerBoard.system_path || innerBoard.repo_path || "",
289
+ default_branch: innerBoard.default_branch || "main",
290
+ agent_instructions: capString(innerBoard.agent_instructions || "", maxCharsPerField)
291
+ },
292
+ ticket: {
293
+ id: ticket.id,
294
+ ticket_id: ticket.id,
295
+ number: ticket.number,
296
+ title: ticket.title,
297
+ type: ticket.type,
298
+ priority: ticket.priority,
299
+ state_id: ticket.state_id,
300
+ state_name: ticket.state_name,
301
+ state_role: ticket.state_role,
302
+ description: capString(ticket.description || "", maxCharsPerField),
303
+ acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
304
+ ai_plan: capString(ticket.ai_plan || "", maxCharsPerField),
305
+ labels
306
+ },
307
+ blockers: {
308
+ can_start: blockers.length === 0,
309
+ blockers
310
+ },
311
+ parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
312
+ relevant_state_ids: relevantStateIds(db, innerBoard.id, ticket.state_id),
313
+ recent_comments: comments
314
+ };
315
+ }
316
+
317
+ function positiveInt(value, fallback) {
318
+ const n = Number(value);
319
+ return Number.isInteger(n) && n > 0 ? n : fallback;
320
+ }
321
+
322
+ function nonNegativeInt(value, fallback) {
323
+ const n = Number(value);
324
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
325
+ }
326
+
327
+ function capString(value, maxChars) {
328
+ const text = String(value || "");
329
+ return maxChars && text.length > maxChars ? text.slice(0, maxChars) : text;
330
+ }
331
+
332
+ function capTicketFields(ticket, maxChars) {
333
+ if (!maxChars) return ticket;
334
+ const copy = { ...ticket };
335
+ for (const field of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
336
+ if (Object.hasOwn(copy, field)) copy[field] = capString(copy[field], maxChars);
337
+ }
338
+ return copy;
339
+ }
340
+
341
+ function extractAcceptance(description) {
342
+ const match = String(description || "").match(/(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i);
343
+ return match ? match[1].trim() : "";
344
+ }
345
+
346
+ function relevantStateIds(db, boardId, currentStateId) {
347
+ const byRole = (role) => stateByRole(db, boardId, role)?.id || null;
348
+ const byName = (name) => stateByName(db, boardId, name)?.id || null;
349
+ return {
350
+ current: currentStateId,
351
+ current_role: stateById(db, currentStateId)?.role || null,
352
+ ai_ready: byRole("ai_ready") || byName("AI Ready"),
353
+ in_progress: byRole("in_progress") || byName("In Progress"),
354
+ review: byRole("review") || byName("Review"),
355
+ done: byRole("done") || byName("Done"),
356
+ todo: byRole("todo") || byName("Todo")
250
357
  };
251
358
  }
package/src/core/board.js CHANGED
@@ -134,7 +134,7 @@ export {
134
134
  export { archiveTicket, createComment, createTicket, deleteTicket, restoreTicket, updateTicket } from "./tickets.js";
135
135
  export { archivedTicketsForBoard } from "./queries.js";
136
136
  export { getTicketBlockers, getTicketRelations } from "./relations.js";
137
- export { checkpointTicket, claimNext, completeTicket, getContextPack, readComments, readTicket } from "./agent.js";
137
+ export { checkpointTicket, claimNext, completeTicket, getAgentDispatchPacket, getContextPack, readComments, readTicket } from "./agent.js";
138
138
  export { searchTickets } from "./search.js";
139
139
  export { exportBoard } from "./snapshots.js";
140
140
 
@@ -65,6 +65,7 @@ import {
65
65
  checkpointTicket,
66
66
  claimNext,
67
67
  completeTicket,
68
+ getAgentDispatchPacket,
68
69
  getContextPack,
69
70
  readComments,
70
71
  readTicket
@@ -104,6 +105,15 @@ function boardHintsFromQuery(url) {
104
105
  };
105
106
  }
106
107
 
108
+ function agentContextOptions(url) {
109
+ return {
110
+ max_chars_per_field: url.searchParams.get("max_chars_per_field"),
111
+ comment_limit: url.searchParams.get("comment_limit"),
112
+ include_parent_full: url.searchParams.get("include_parent_full") === "true",
113
+ include_related_full: url.searchParams.get("include_related_full") === "true"
114
+ };
115
+ }
116
+
107
117
  export async function handleApi(req, res, url) {
108
118
  setCors(res);
109
119
 
@@ -269,7 +279,11 @@ export async function handleApi(req, res, url) {
269
279
  }
270
280
  if (sub === "context" && req.method === "GET") {
271
281
  const depth = Number(url.searchParams.get("depth") || 1);
272
- sendJson(res, 200, getContextPack(ticketId, ctx, depth));
282
+ sendJson(res, 200, getContextPack(ticketId, ctx, depth, agentContextOptions(url)));
283
+ return;
284
+ }
285
+ if ((sub === "dispatch-packet" || sub === "agent-dispatch-packet") && req.method === "GET") {
286
+ sendJson(res, 200, getAgentDispatchPacket(ticketId, ctx, agentContextOptions(url)));
273
287
  return;
274
288
  }
275
289
  if (sub === "history" && req.method === "GET") {
@@ -399,7 +413,11 @@ export async function handleApi(req, res, url) {
399
413
  if (!row) throw httpError(400, "board_id_required");
400
414
  const args = {
401
415
  q: url.searchParams.get("q") || "",
402
- limit: url.searchParams.get("limit")
416
+ limit: url.searchParams.get("limit"),
417
+ mode: url.searchParams.get("mode"),
418
+ include_full: url.searchParams.get("include_full") === "true" ? true : undefined,
419
+ fields: url.searchParams.get("fields"),
420
+ max_chars_per_field: url.searchParams.get("max_chars_per_field")
403
421
  };
404
422
  sendJson(res, 200, searchTickets(args, ctxFromBoardRow(row, actor)));
405
423
  return;
@@ -12,9 +12,12 @@ export function searchTickets(args, ctx) {
12
12
  const { db, board, actor } = ctx;
13
13
  const q = String(args.q || "").trim();
14
14
  const limit = Math.min(Number(args.limit || 20), 50);
15
+ const mode = normalizeMode(args);
16
+ const maxCharsPerField = positiveInt(args.max_chars_per_field ?? args.maxCharsPerField, 0);
17
+ const fields = parseFields(args.fields);
15
18
 
16
- if (!q) return { query: q, results: [] };
17
- if (!canAccessBoard(actor, board)) return { query: q, results: [] };
19
+ if (!q) return { query: q, mode, results: [] };
20
+ if (!canAccessBoard(actor, board)) return { query: q, mode, results: [] };
18
21
 
19
22
  const ticketNumber = ticketNumberQuery(q);
20
23
  const fts = toFtsQuery(q);
@@ -93,12 +96,82 @@ export function searchTickets(args, ctx) {
93
96
  if (deduped.length >= limit) break;
94
97
  }
95
98
 
96
- const results = deduped.map((ticket) => ({
97
- ...ticket,
98
- labels: labelsForTicket(db, ticket.id)
99
- }));
99
+ const results = deduped.map((ticket) =>
100
+ shapeSearchResult(ticket, labelsForTicket(db, ticket.id), { mode, fields, maxCharsPerField })
101
+ );
100
102
 
101
- return { query: q, results };
103
+ return { query: q, mode, results };
104
+ }
105
+
106
+ function normalizeMode(args) {
107
+ if (args.include_full === true || args.includeFull === true) return "full";
108
+ const mode = String(args.mode || "summary").trim().toLowerCase();
109
+ return ["ids", "summary", "full"].includes(mode) ? mode : "summary";
110
+ }
111
+
112
+ function positiveInt(value, fallback) {
113
+ const n = Number(value);
114
+ return Number.isInteger(n) && n > 0 ? n : fallback;
115
+ }
116
+
117
+ function parseFields(fields) {
118
+ if (Array.isArray(fields)) return new Set(fields.map(String));
119
+ if (typeof fields === "string" && fields.trim()) {
120
+ return new Set(fields.split(",").map((field) => field.trim()).filter(Boolean));
121
+ }
122
+ return null;
123
+ }
124
+
125
+ function cap(value, maxChars) {
126
+ if (!maxChars || typeof value !== "string") return value;
127
+ return value.length > maxChars ? value.slice(0, maxChars) : value;
128
+ }
129
+
130
+ function snippetFor(ticket, maxChars) {
131
+ const text = ticket.description || ticket.ai_plan || ticket.implementation_summary || "";
132
+ return cap(text, maxChars || 240);
133
+ }
134
+
135
+ function labelNames(labels) {
136
+ return labels.map((label) => label.name);
137
+ }
138
+
139
+ function pickFields(row, fields) {
140
+ if (!fields) return row;
141
+ const picked = {};
142
+ for (const field of fields) {
143
+ if (Object.hasOwn(row, field)) picked[field] = row[field];
144
+ }
145
+ return picked;
146
+ }
147
+
148
+ function shapeSearchResult(ticket, labels, { mode, fields, maxCharsPerField }) {
149
+ if (mode === "ids") {
150
+ return pickFields({ ticket_id: ticket.id, id: ticket.id, number: ticket.number, rank: ticket.rank }, fields);
151
+ }
152
+ if (mode === "full") {
153
+ const full = { ...ticket, labels };
154
+ if (maxCharsPerField) {
155
+ for (const key of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
156
+ full[key] = cap(full[key], maxCharsPerField);
157
+ }
158
+ }
159
+ return pickFields(full, fields);
160
+ }
161
+ return pickFields({
162
+ ticket_id: ticket.id,
163
+ id: ticket.id,
164
+ number: ticket.number,
165
+ title: ticket.title,
166
+ state_name: ticket.state_name,
167
+ state_role: ticket.state_role,
168
+ type: ticket.type,
169
+ priority: ticket.priority,
170
+ parent_ticket_id: ticket.parent_ticket_id,
171
+ rank: ticket.rank,
172
+ snippet: snippetFor(ticket, maxCharsPerField),
173
+ labels: labelNames(labels)
174
+ }, fields);
102
175
  }
103
176
 
104
177
  function ticketNumberQuery(q) {
@@ -57,6 +57,24 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
57
57
  }
58
58
 
59
59
  const ticketQuery = (args = {}) => ({ board_id: args.board_id, board: args.board_slug || args.board || defaultBoard });
60
+ const searchQuery = (args = {}) => ({
61
+ q: args.q,
62
+ limit: args.limit,
63
+ board_id: args.board_id,
64
+ board: args.board_slug || args.board || defaultBoard,
65
+ mode: args.mode,
66
+ include_full: args.include_full || args.includeFull ? "true" : undefined,
67
+ fields: Array.isArray(args.fields) ? args.fields.join(",") : args.fields,
68
+ max_chars_per_field: args.max_chars_per_field ?? args.maxCharsPerField
69
+ });
70
+ const contextQuery = (args = {}) => ({
71
+ ...ticketQuery(args),
72
+ depth: args.depth,
73
+ max_chars_per_field: args.max_chars_per_field ?? args.maxCharsPerField,
74
+ comment_limit: args.comment_limit ?? args.commentLimit,
75
+ include_parent_full: args.include_parent_full || args.includeParentFull ? "true" : undefined,
76
+ include_related_full: args.include_related_full || args.includeRelatedFull ? "true" : undefined
77
+ });
60
78
 
61
79
  return {
62
80
  mode: "remote",
@@ -68,7 +86,8 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
68
86
  async boardList() { return request("GET", "/api/boards"); },
69
87
  async boardSetActive(args = {}) { if (!args.slug) throw rpcError(-32602, "slug is required."); env.ORBIT_DEFAULT_BOARD = String(args.slug); return { ok: true, slug: String(args.slug), mode: "remote" }; },
70
88
  async claimNext(args = {}) { return request("POST", "/api/agent/claim-next", { body: { ...args, board: requireBoard(args) } }); },
71
- async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...ticketQuery(args), depth: args.depth || 1 } }); },
89
+ async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
90
+ async getAgentDispatchPacket(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/dispatch-packet`, { query: contextQuery(args) }); },
72
91
  async readTicket(args = {}) {
73
92
  if (args.ticket_id) {
74
93
  return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) });
@@ -92,7 +111,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
92
111
  },
93
112
  async getTicketRelations(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/relations`, { query: ticketQuery(args) }); },
94
113
  async getTicketBlockers(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/blockers`, { query: ticketQuery(args) }); },
95
- async search(args = {}) { return request("GET", "/api/search", { query: { q: args.q, limit: args.limit, board_id: args.board_id, board: args.board_slug || args.board || defaultBoard } }); },
114
+ async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
96
115
  async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
97
116
  async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
98
117
  async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
@@ -196,12 +215,13 @@ export async function createLocalOrbitClient(env = process.env) {
196
215
  boardList: () => ({ boards: registry.listBoards().map((row) => ({ id: row.id, slug: row.slug, name: row.name, repo_path: row.repo_path })) }),
197
216
  boardSetActive: (args = {}) => { const slug = String(args.slug || "").trim(); if (!slug) throw rpcError(-32602, "slug is required."); const row = registry.getBoardBySlug(slug); if (!row) throw rpcError(-32004, `Board slug not found: ${slug}`); setSessionBoard(row); return { ok: true, board_id: row.id, slug: row.slug, name: row.name, db_path: row.db_path }; },
198
217
  claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
199
- getTicketContext: (args = {}) => board.getContextPack(args.ticket_id, sessionCtx(actor()), Number(args.depth || 1)),
200
- readTicket: (args = {}) => board.readTicket(args, sessionCtx(actor())),
201
- readComments: (args = {}) => board.readComments(args, sessionCtx(actor())),
202
- getTicketRelations: (args = {}) => board.getTicketRelations(args.ticket_id, sessionCtx(actor())),
203
- getTicketBlockers: (args = {}) => board.getTicketBlockers(args.ticket_id, sessionCtx(actor())),
204
- search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets({ q: args.q, limit: args.limit }, ctx); },
218
+ getTicketContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPack(args.ticket_id, ctx, Number(args.depth || 1), args); },
219
+ getAgentDispatchPacket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getAgentDispatchPacket(args.ticket_id, ctx, args); },
220
+ readTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readTicket(args, ctx); },
221
+ readComments: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readComments(args, ctx); },
222
+ getTicketRelations: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketRelations(args.ticket_id, ctx); },
223
+ getTicketBlockers: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketBlockers(args.ticket_id, ctx); },
224
+ search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
205
225
  createTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const { board_slug, board: _b, ...body } = args; body.board_id = ctx.board.id; return backup(ctx, board.createTicket(body, ctx)); },
206
226
  updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
207
227
  addComment: ({ ticket_id, ...body }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.createComment(ticket_id, body, ctx)); },
package/src/mcp-server.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  deleteTicket,
20
20
  exportBoard,
21
21
  getBoardContext,
22
+ getAgentDispatchPacket,
22
23
  getContextPack,
23
24
  readComments,
24
25
  readTicket,
@@ -247,13 +248,38 @@ const TOOL_DEFS = [
247
248
  type: "object",
248
249
  properties: {
249
250
  ticket_id: { type: "string" },
250
- depth: { type: "integer", minimum: 1, maximum: 3 }
251
+ board_id: { type: "string" },
252
+ board_slug: { type: "string" },
253
+ board: { type: "string" },
254
+ depth: { type: "integer", minimum: 1, maximum: 3 },
255
+ max_chars_per_field: { type: "integer", minimum: 1 },
256
+ include_parent_full: { type: "boolean" },
257
+ include_related_full: { type: "boolean" }
251
258
  },
252
259
  required: ["ticket_id"],
253
260
  additionalProperties: false
254
261
  },
255
262
  handler: (args) => orbitClient.getTicketContext(args)
256
263
  },
264
+ {
265
+ name: "board_get_agent_dispatch_packet",
266
+ description:
267
+ "Return a lean agent dispatch packet for one ticket: board identity/instructions, capped ticket description/acceptance/AI plan, blockers, shallow parent, relevant workflow state IDs, repo path/default branch, recent capped comments, and label names. Parent/related/implementation bodies are intentionally omitted.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ ticket_id: { type: "string" },
272
+ board_id: { type: "string" },
273
+ board_slug: { type: "string" },
274
+ board: { type: "string" },
275
+ max_chars_per_field: { type: "integer", minimum: 1 },
276
+ comment_limit: { type: "integer", minimum: 0, maximum: 20 }
277
+ },
278
+ required: ["ticket_id"],
279
+ additionalProperties: false
280
+ },
281
+ handler: (args) => orbitClient.getAgentDispatchPacket(args)
282
+ },
257
283
  {
258
284
  name: "board_read_ticket",
259
285
  description:
@@ -263,7 +289,10 @@ const TOOL_DEFS = [
263
289
  properties: {
264
290
  ticket_id: { type: "string" },
265
291
  number: { type: "integer" },
266
- title: { type: "string" }
292
+ title: { type: "string" },
293
+ board_id: { type: "string" },
294
+ board_slug: { type: "string" },
295
+ board: { type: "string" }
267
296
  },
268
297
  additionalProperties: false
269
298
  },
@@ -278,7 +307,10 @@ const TOOL_DEFS = [
278
307
  properties: {
279
308
  ticket_id: { type: "string" },
280
309
  number: { type: "integer" },
281
- title: { type: "string" }
310
+ title: { type: "string" },
311
+ board_id: { type: "string" },
312
+ board_slug: { type: "string" },
313
+ board: { type: "string" }
282
314
  },
283
315
  additionalProperties: false
284
316
  },
@@ -290,7 +322,10 @@ const TOOL_DEFS = [
290
322
  inputSchema: {
291
323
  type: "object",
292
324
  properties: {
293
- ticket_id: { type: "string" }
325
+ ticket_id: { type: "string" },
326
+ board_id: { type: "string" },
327
+ board_slug: { type: "string" },
328
+ board: { type: "string" }
294
329
  },
295
330
  required: ["ticket_id"],
296
331
  additionalProperties: false
@@ -303,7 +338,10 @@ const TOOL_DEFS = [
303
338
  inputSchema: {
304
339
  type: "object",
305
340
  properties: {
306
- ticket_id: { type: "string" }
341
+ ticket_id: { type: "string" },
342
+ board_id: { type: "string" },
343
+ board_slug: { type: "string" },
344
+ board: { type: "string" }
307
345
  },
308
346
  required: ["ticket_id"],
309
347
  additionalProperties: false
@@ -321,7 +359,11 @@ const TOOL_DEFS = [
321
359
  board: { type: "string" },
322
360
  board_slug: { type: "string" },
323
361
  board_id: { type: "string" },
324
- limit: { type: "integer", minimum: 1, maximum: 50 }
362
+ limit: { type: "integer", minimum: 1, maximum: 50 },
363
+ mode: { type: "string", enum: ["ids", "summary", "full"] },
364
+ include_full: { type: "boolean" },
365
+ fields: { type: "array", items: { type: "string" } },
366
+ max_chars_per_field: { type: "integer", minimum: 1 }
325
367
  },
326
368
  required: ["q"],
327
369
  additionalProperties: false