@starlove/orbit 1.1.0 → 1.1.2
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/README.md +2 -2
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +22 -6
- package/package.json +1 -1
- package/public/js/local-backend.js +15 -4
- package/src/core/agent.js +138 -10
- package/src/core/board.js +1 -1
- package/src/core/dispatch.js +2 -2
- package/src/core/router.js +26 -2
- package/src/core/search.js +80 -7
- package/src/mcp/orbit-client.js +30 -8
- package/src/mcp-server.js +71 -8
package/README.md
CHANGED
|
@@ -176,8 +176,8 @@ A copy-editable client config lives at [docs/mcp-client.example.json](docs/mcp-c
|
|
|
176
176
|
| Purpose | Tool |
|
|
177
177
|
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
178
178
|
| Index | `board_list` (tiny registry), `board_set_active` |
|
|
179
|
-
| Board context | `board_context` (settings + journal + deployment; optional board + `include_struck`)
|
|
180
|
-
| Read | `board_read_ticket`, `board_read_comments`, `board_get_ticket_context` (
|
|
179
|
+
| Board context | `board_context` (settings + journal + deployment; optional board + `include_struck`) |
|
|
180
|
+
| Read | `board_read_ticket`, `board_read_comments`, `board_get_ticket_context` (lean), `board_get_ticket_context_full` (manual/journal), `board_get_ticket_relations`, `board_get_ticket_blockers`, `board_search`, `board_list_archive` |
|
|
181
181
|
| Agent flow | `board_claim_next`, `board_checkpoint`, `board_complete` |
|
|
182
182
|
| Write | `board_update_ticket`, `board_add_comment`, `board_add_board_entry`, `board_archive_ticket`, `board_restore_ticket`, `board_delete_ticket` |
|
|
183
183
|
| Admin | `board_export_board`, `board_update_settings` |
|
package/dist/full/version.json
CHANGED
package/docs/AGENT_PROTOCOL.md
CHANGED
|
@@ -72,7 +72,7 @@ Returns the first schedulable `AI Ready` ticket. The scheduler skips tickets blo
|
|
|
72
72
|
|
|
73
73
|
By default, `claim-next` skips `epic` cards because they are index/planning cards. To claim an epic for planning, pass `"type": "epic"` or `"include_epics": true`.
|
|
74
74
|
|
|
75
|
-
The response shape is `{ claimed: true, ticket_id, context }` where `context` is the lightweight `board_read_ticket` pack (`ticket` with title/description/labels/state and the `board_manual`). Ticket comments are not included in the default claim context; call `GET /api/tickets/:id/comments` (or MCP `board_read_comments`) when an explicit chat/comment surface needs them. For the
|
|
75
|
+
The response shape is `{ claimed: true, ticket_id, context }` where `context` is the lightweight `board_read_ticket` pack (`ticket` with title/description/labels/state and the `board_manual`). Ticket comments are not included in the default claim context; call `GET /api/tickets/:id/comments` (or MCP `board_read_comments`) when an explicit chat/comment surface needs them. For the lean ticket relationship pack, call `GET /api/tickets/:id/context` (or MCP `board_get_ticket_context`). For the historical heavy pack with board manual/journal attached, call `GET /api/tickets/:id/context/full` (or MCP `board_get_ticket_context_full`).
|
|
76
76
|
|
|
77
77
|
### Read Lightweight Ticket
|
|
78
78
|
|
|
@@ -86,7 +86,7 @@ MCP equivalent: `board_read_ticket`.
|
|
|
86
86
|
|
|
87
87
|
Returns the lightweight agent read shape: `ticket` with identity, title, description, labels, state/type/priority, plus `board_manual`. Ticket comments, relations, blockers, child cards, and implementation fields are intentionally omitted. Use this for quick orientation and claim context. The `lookup` form is exact by per-board ticket number or exact title; it does not use fuzzy search or comment/implementation indexes.
|
|
88
88
|
|
|
89
|
-
### Get Context Pack
|
|
89
|
+
### Get Lean Ticket Context Pack
|
|
90
90
|
|
|
91
91
|
```http
|
|
92
92
|
GET /api/tickets/:id/context?depth=1
|
|
@@ -94,15 +94,31 @@ GET /api/tickets/:id/context?depth=1
|
|
|
94
94
|
|
|
95
95
|
Returns:
|
|
96
96
|
|
|
97
|
-
- Ticket, board, state, labels.
|
|
98
|
-
- The board manual (`board_manual`), including `agent_instructions` and `project_notes`.
|
|
97
|
+
- Ticket, board identity, state, labels.
|
|
99
98
|
- Dedicated implementation fields: `ai_plan`, `implementation_summary`, and `implementation_updates`.
|
|
100
99
|
- Parent epic/story ticket when present.
|
|
101
100
|
- Child feature cards when the ticket is an epic/story.
|
|
102
101
|
- Related tickets.
|
|
103
102
|
- Blockers and blocking tickets.
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
Project manual/journal (`agent_instructions`, `project_notes`, and board entries) and ticket comments are intentionally omitted from this default agent context. Fetch project context and comments explicitly when needed:
|
|
105
|
+
|
|
106
|
+
```http
|
|
107
|
+
GET /api/boards/:board_id/context?include_struck=false
|
|
108
|
+
GET /api/tickets/:id/comments
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
MCP equivalents: `board_context` and `board_read_comments`.
|
|
112
|
+
|
|
113
|
+
### Get Full Ticket Context Pack
|
|
114
|
+
|
|
115
|
+
```http
|
|
116
|
+
GET /api/tickets/:id/context/full?depth=1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
MCP equivalent: `board_get_ticket_context_full`.
|
|
120
|
+
|
|
121
|
+
Returns the historical heavy context shape: everything in the lean ticket context plus the full board row and `board_manual` / journal. Comments are still intentionally omitted; explicit comment/chat consumers should call:
|
|
106
122
|
|
|
107
123
|
```http
|
|
108
124
|
GET /api/tickets/:id/comments
|
|
@@ -176,7 +192,7 @@ Searches ticket titles, descriptions, and comments.
|
|
|
176
192
|
GET /api/boards/:board_id/context?include_struck=false
|
|
177
193
|
```
|
|
178
194
|
|
|
179
|
-
Returns the same shape as MCP `board_context`:
|
|
195
|
+
Returns the same shape as MCP `board_context`: board metadata (`agent_instructions`, `project_notes`, repository paths, default branch), `entries` (journal / decisions / lessons), and `deployment` paths. Use `include_struck=true` to include struck journal rows (default off).
|
|
180
196
|
|
|
181
197
|
MCP: `board_context` with optional `board_id` / `board_slug` / `board` and optional `include_struck`.
|
|
182
198
|
|
package/package.json
CHANGED
|
@@ -726,7 +726,7 @@ async function handleTicketArchive(ticketId, archive) {
|
|
|
726
726
|
});
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
-
async function handleTicketContext(ticketId) {
|
|
729
|
+
async function handleTicketContext(ticketId, { includeProjectContext = false } = {}) {
|
|
730
730
|
const db = await openDb();
|
|
731
731
|
const snap = await readBoardSnapshot(db);
|
|
732
732
|
const ticket = snap.tickets.find((t) => t.id === ticketId);
|
|
@@ -776,16 +776,26 @@ async function handleTicketContext(ticketId) {
|
|
|
776
776
|
other_ticket: compact(other)
|
|
777
777
|
};
|
|
778
778
|
});
|
|
779
|
-
|
|
779
|
+
const context = {
|
|
780
780
|
ticket: { ...ticket, labels },
|
|
781
|
-
board:
|
|
782
|
-
|
|
781
|
+
board: includeProjectContext
|
|
782
|
+
? snap.board
|
|
783
|
+
: {
|
|
784
|
+
id: snap.board.id,
|
|
785
|
+
slug: snap.board.slug,
|
|
786
|
+
name: snap.board.name,
|
|
787
|
+
repo_path: snap.board.system_path || "",
|
|
788
|
+
system_path: snap.board.system_path || "",
|
|
789
|
+
default_branch: snap.board.default_branch || "main"
|
|
790
|
+
},
|
|
783
791
|
parent_ticket: compact(parent),
|
|
784
792
|
child_tickets: childTickets,
|
|
785
793
|
relations,
|
|
786
794
|
blockers: [],
|
|
787
795
|
related_tickets: [...childTickets, ...(parent ? [parent] : [])]
|
|
788
796
|
};
|
|
797
|
+
if (includeProjectContext) context.board_manual = { entries: snap.boardEntries.filter((e) => !e.struck_at) };
|
|
798
|
+
return context;
|
|
789
799
|
}
|
|
790
800
|
|
|
791
801
|
async function handleTicketComments(ticketId) {
|
|
@@ -955,6 +965,7 @@ const ROUTES = [
|
|
|
955
965
|
{ m: "DELETE", re: /^\/api\/tickets\/([^/]+)$/, fn: (m) => handleTicketDelete(decodeURIComponent(m[1])) },
|
|
956
966
|
{ m: "POST", re: /^\/api\/tickets\/([^/]+)\/archive$/, fn: (m) => handleTicketArchive(decodeURIComponent(m[1]), true) },
|
|
957
967
|
{ m: "POST", re: /^\/api\/tickets\/([^/]+)\/restore$/, fn: (m) => handleTicketArchive(decodeURIComponent(m[1]), false) },
|
|
968
|
+
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/context\/full$/, fn: (m) => handleTicketContext(decodeURIComponent(m[1]), { includeProjectContext: true }) },
|
|
958
969
|
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/context$/, fn: (m) => handleTicketContext(decodeURIComponent(m[1])) },
|
|
959
970
|
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/comments$/, fn: (m) => handleTicketComments(decodeURIComponent(m[1])) },
|
|
960
971
|
{ m: "GET", re: /^\/api\/tickets\/([^/]+)\/history$/, fn: (m) => handleTicketHistory(decodeURIComponent(m[1])) },
|
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,22 @@ export function completeTicket(body, ctx) {
|
|
|
209
210
|
return getContextPack(ticket.id, ctx, 1);
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
function contextBoardIdentity(innerBoard, ctx) {
|
|
214
|
+
return {
|
|
215
|
+
id: innerBoard.id,
|
|
216
|
+
slug: innerBoard.slug,
|
|
217
|
+
name: innerBoard.name,
|
|
218
|
+
repo_path: innerBoard.system_path || ctx.board.repo_path || "",
|
|
219
|
+
system_path: innerBoard.system_path || ctx.board.repo_path || "",
|
|
220
|
+
default_branch: innerBoard.default_branch || "main"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildContextPack(ticketId, ctx, depth = 1, options = {}, { includeProjectContext = false } = {}) {
|
|
213
225
|
const { db, board, actor } = ctx;
|
|
226
|
+
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
|
|
227
|
+
const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
|
|
228
|
+
const includeRelatedFull = Boolean(options.include_related_full ?? options.includeRelatedFull);
|
|
214
229
|
const ticket = ticketById(db, ticketId);
|
|
215
230
|
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
216
231
|
const innerBoard = boardById(db, ticket.board_id);
|
|
@@ -232,20 +247,133 @@ export function getContextPack(ticketId, ctx, depth = 1) {
|
|
|
232
247
|
...(parentTicket ? [parentTicket] : [])
|
|
233
248
|
]);
|
|
234
249
|
|
|
235
|
-
|
|
236
|
-
ticket: {
|
|
250
|
+
const pack = {
|
|
251
|
+
ticket: capTicketFields({
|
|
237
252
|
...ticket,
|
|
238
253
|
labels
|
|
239
|
-
},
|
|
240
|
-
board: innerBoard,
|
|
241
|
-
|
|
242
|
-
parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
|
|
254
|
+
}, maxCharsPerField),
|
|
255
|
+
board: includeProjectContext ? innerBoard : contextBoardIdentity(innerBoard, ctx),
|
|
256
|
+
parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
|
|
243
257
|
child_tickets: childTickets.map((child) => ({
|
|
244
|
-
...child,
|
|
258
|
+
...capTicketFields(child, maxCharsPerField),
|
|
245
259
|
child_count: childTicketsFor(db, child.id).length
|
|
246
260
|
})),
|
|
247
|
-
relations
|
|
261
|
+
relations: relations.map((relation) => ({
|
|
262
|
+
...relation,
|
|
263
|
+
other_ticket: includeRelatedFull
|
|
264
|
+
? capTicketFields({ ...relation.other_ticket, labels: labelsForTicket(db, relation.other_ticket.id) }, maxCharsPerField)
|
|
265
|
+
: compactTicket(relation.other_ticket)
|
|
266
|
+
})),
|
|
248
267
|
blockers,
|
|
249
|
-
related_tickets: relatedTickets
|
|
268
|
+
related_tickets: relatedTickets.map((related) =>
|
|
269
|
+
includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
|
|
270
|
+
)
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (includeProjectContext) pack.board_manual = boardManual(innerBoard.id, ctx);
|
|
274
|
+
return pack;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
|
|
278
|
+
return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: false });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function getContextPackFull(ticketId, ctx, depth = 1, options = {}) {
|
|
282
|
+
return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
|
|
286
|
+
const { db, board, actor } = ctx;
|
|
287
|
+
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 4000);
|
|
288
|
+
const commentLimit = Math.min(nonNegativeInt(options.comment_limit ?? options.commentLimit, 5), 20);
|
|
289
|
+
const ticket = ticketById(db, ticketId);
|
|
290
|
+
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
291
|
+
const innerBoard = boardById(db, ticket.board_id);
|
|
292
|
+
requireBoardAccess(actor, innerBoard);
|
|
293
|
+
|
|
294
|
+
const labels = labelsForTicket(db, ticket.id).map((label) => label.name);
|
|
295
|
+
const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
|
|
296
|
+
const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
|
|
297
|
+
const comments = db
|
|
298
|
+
.prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
299
|
+
.all(ticket.id, commentLimit)
|
|
300
|
+
.reverse()
|
|
301
|
+
.map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
board: {
|
|
305
|
+
id: innerBoard.id,
|
|
306
|
+
slug: innerBoard.slug,
|
|
307
|
+
name: innerBoard.name,
|
|
308
|
+
repo_path: innerBoard.repo_path || innerBoard.system_path || "",
|
|
309
|
+
system_path: innerBoard.system_path || innerBoard.repo_path || "",
|
|
310
|
+
default_branch: innerBoard.default_branch || "main",
|
|
311
|
+
agent_instructions: capString(innerBoard.agent_instructions || "", maxCharsPerField)
|
|
312
|
+
},
|
|
313
|
+
ticket: {
|
|
314
|
+
id: ticket.id,
|
|
315
|
+
ticket_id: ticket.id,
|
|
316
|
+
number: ticket.number,
|
|
317
|
+
title: ticket.title,
|
|
318
|
+
type: ticket.type,
|
|
319
|
+
priority: ticket.priority,
|
|
320
|
+
state_id: ticket.state_id,
|
|
321
|
+
state_name: ticket.state_name,
|
|
322
|
+
state_role: ticket.state_role,
|
|
323
|
+
description: capString(ticket.description || "", maxCharsPerField),
|
|
324
|
+
acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
|
|
325
|
+
ai_plan: capString(ticket.ai_plan || "", maxCharsPerField),
|
|
326
|
+
labels
|
|
327
|
+
},
|
|
328
|
+
blockers: {
|
|
329
|
+
can_start: blockers.length === 0,
|
|
330
|
+
blockers
|
|
331
|
+
},
|
|
332
|
+
parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
|
|
333
|
+
relevant_state_ids: relevantStateIds(db, innerBoard.id, ticket.state_id),
|
|
334
|
+
recent_comments: comments
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function positiveInt(value, fallback) {
|
|
339
|
+
const n = Number(value);
|
|
340
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function nonNegativeInt(value, fallback) {
|
|
344
|
+
const n = Number(value);
|
|
345
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function capString(value, maxChars) {
|
|
349
|
+
const text = String(value || "");
|
|
350
|
+
return maxChars && text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function capTicketFields(ticket, maxChars) {
|
|
354
|
+
if (!maxChars) return ticket;
|
|
355
|
+
const copy = { ...ticket };
|
|
356
|
+
for (const field of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
|
|
357
|
+
if (Object.hasOwn(copy, field)) copy[field] = capString(copy[field], maxChars);
|
|
358
|
+
}
|
|
359
|
+
return copy;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function extractAcceptance(description) {
|
|
363
|
+
const match = String(description || "").match(/(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i);
|
|
364
|
+
return match ? match[1].trim() : "";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function relevantStateIds(db, boardId, currentStateId) {
|
|
368
|
+
const byRole = (role) => stateByRole(db, boardId, role)?.id || null;
|
|
369
|
+
const byName = (name) => stateByName(db, boardId, name)?.id || null;
|
|
370
|
+
return {
|
|
371
|
+
current: currentStateId,
|
|
372
|
+
current_role: stateById(db, currentStateId)?.role || null,
|
|
373
|
+
ai_ready: byRole("ai_ready") || byName("AI Ready"),
|
|
374
|
+
in_progress: byRole("in_progress") || byName("In Progress"),
|
|
375
|
+
review: byRole("review") || byName("Review"),
|
|
376
|
+
done: byRole("done") || byName("Done"),
|
|
377
|
+
todo: byRole("todo") || byName("Todo")
|
|
250
378
|
};
|
|
251
379
|
}
|
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, getContextPackFull, readComments, readTicket } from "./agent.js";
|
|
138
138
|
export { searchTickets } from "./search.js";
|
|
139
139
|
export { exportBoard } from "./snapshots.js";
|
|
140
140
|
|
package/src/core/dispatch.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|
|
3
3
|
import { extname, join, resolve } from "node:path";
|
|
4
4
|
import { localOwnerActor } from "./auth.js";
|
|
5
5
|
import { DISPATCH_RUNS_DIR } from "./paths.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getContextPackFull } from "./agent.js";
|
|
7
7
|
import { openBoardDb, getBoardByRegistryId, getBoardByRepoPath, getBoardBySlug, touchBoardActive } from "./registry.js";
|
|
8
8
|
import { stateByName, stateByRole, ticketById, ticketByNumber, unresolvedBlockers } from "./queries.js";
|
|
9
9
|
import { createRegistrySchema } from "./db.js";
|
|
@@ -438,7 +438,7 @@ export function dispatchTicket(options) {
|
|
|
438
438
|
|
|
439
439
|
mkdirSync(runDir, { recursive: true });
|
|
440
440
|
const policyBin = createPolicyBin(runDir, policyName);
|
|
441
|
-
const context =
|
|
441
|
+
const context = getContextPackFull(ticket.id, ctx, 1);
|
|
442
442
|
const handoff = buildHandoff({
|
|
443
443
|
boardRow,
|
|
444
444
|
context,
|
package/src/core/router.js
CHANGED
|
@@ -65,7 +65,9 @@ import {
|
|
|
65
65
|
checkpointTicket,
|
|
66
66
|
claimNext,
|
|
67
67
|
completeTicket,
|
|
68
|
+
getAgentDispatchPacket,
|
|
68
69
|
getContextPack,
|
|
70
|
+
getContextPackFull,
|
|
69
71
|
readComments,
|
|
70
72
|
readTicket
|
|
71
73
|
} from "./agent.js";
|
|
@@ -104,6 +106,15 @@ function boardHintsFromQuery(url) {
|
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
function agentContextOptions(url) {
|
|
110
|
+
return {
|
|
111
|
+
max_chars_per_field: url.searchParams.get("max_chars_per_field"),
|
|
112
|
+
comment_limit: url.searchParams.get("comment_limit"),
|
|
113
|
+
include_parent_full: url.searchParams.get("include_parent_full") === "true",
|
|
114
|
+
include_related_full: url.searchParams.get("include_related_full") === "true"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
export async function handleApi(req, res, url) {
|
|
108
119
|
setCors(res);
|
|
109
120
|
|
|
@@ -269,7 +280,16 @@ export async function handleApi(req, res, url) {
|
|
|
269
280
|
}
|
|
270
281
|
if (sub === "context" && req.method === "GET") {
|
|
271
282
|
const depth = Number(url.searchParams.get("depth") || 1);
|
|
272
|
-
sendJson(res, 200, getContextPack(ticketId, ctx, depth));
|
|
283
|
+
sendJson(res, 200, getContextPack(ticketId, ctx, depth, agentContextOptions(url)));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (sub === "context/full" && req.method === "GET") {
|
|
287
|
+
const depth = Number(url.searchParams.get("depth") || 1);
|
|
288
|
+
sendJson(res, 200, getContextPackFull(ticketId, ctx, depth, agentContextOptions(url)));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if ((sub === "dispatch-packet" || sub === "agent-dispatch-packet") && req.method === "GET") {
|
|
292
|
+
sendJson(res, 200, getAgentDispatchPacket(ticketId, ctx, agentContextOptions(url)));
|
|
273
293
|
return;
|
|
274
294
|
}
|
|
275
295
|
if (sub === "history" && req.method === "GET") {
|
|
@@ -399,7 +419,11 @@ export async function handleApi(req, res, url) {
|
|
|
399
419
|
if (!row) throw httpError(400, "board_id_required");
|
|
400
420
|
const args = {
|
|
401
421
|
q: url.searchParams.get("q") || "",
|
|
402
|
-
limit: url.searchParams.get("limit")
|
|
422
|
+
limit: url.searchParams.get("limit"),
|
|
423
|
+
mode: url.searchParams.get("mode"),
|
|
424
|
+
include_full: url.searchParams.get("include_full") === "true" ? true : undefined,
|
|
425
|
+
fields: url.searchParams.get("fields"),
|
|
426
|
+
max_chars_per_field: url.searchParams.get("max_chars_per_field")
|
|
403
427
|
};
|
|
404
428
|
sendJson(res, 200, searchTickets(args, ctxFromBoardRow(row, actor)));
|
|
405
429
|
return;
|
package/src/core/search.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
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) {
|
package/src/mcp/orbit-client.js
CHANGED
|
@@ -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,9 @@ 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: { ...
|
|
89
|
+
async getTicketContext(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
|
|
90
|
+
async getTicketContextFull(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/context/full`, { query: { ...contextQuery(args), depth: args.depth || 1 } }); },
|
|
91
|
+
async getAgentDispatchPacket(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/dispatch-packet`, { query: contextQuery(args) }); },
|
|
72
92
|
async readTicket(args = {}) {
|
|
73
93
|
if (args.ticket_id) {
|
|
74
94
|
return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}`, { query: ticketQuery(args) });
|
|
@@ -92,7 +112,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
|
|
|
92
112
|
},
|
|
93
113
|
async getTicketRelations(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/relations`, { query: ticketQuery(args) }); },
|
|
94
114
|
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:
|
|
115
|
+
async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
|
|
96
116
|
async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
|
|
97
117
|
async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: patch }); },
|
|
98
118
|
async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { body, ok: [201] }); },
|
|
@@ -196,12 +216,14 @@ export async function createLocalOrbitClient(env = process.env) {
|
|
|
196
216
|
boardList: () => ({ boards: registry.listBoards().map((row) => ({ id: row.id, slug: row.slug, name: row.name, repo_path: row.repo_path })) }),
|
|
197
217
|
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
218
|
claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
|
|
199
|
-
getTicketContext: (args = {}) => board.getContextPack(args.ticket_id,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
getTicketContext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPack(args.ticket_id, ctx, Number(args.depth || 1), args); },
|
|
220
|
+
getTicketContextFull: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getContextPackFull(args.ticket_id, ctx, Number(args.depth || 1), args); },
|
|
221
|
+
getAgentDispatchPacket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getAgentDispatchPacket(args.ticket_id, ctx, args); },
|
|
222
|
+
readTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readTicket(args, ctx); },
|
|
223
|
+
readComments: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readComments(args, ctx); },
|
|
224
|
+
getTicketRelations: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketRelations(args.ticket_id, ctx); },
|
|
225
|
+
getTicketBlockers: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getTicketBlockers(args.ticket_id, ctx); },
|
|
226
|
+
search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
|
|
205
227
|
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
228
|
updateTicket: ({ ticket_id, ...patch }) => { const ctx = sessionCtx(actor()); return backup(ctx, board.updateTicket(ticket_id, patch, ctx)); },
|
|
207
229
|
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,
|
|
@@ -184,7 +185,7 @@ const TOOL_DEFS = [
|
|
|
184
185
|
{
|
|
185
186
|
name: "board_context",
|
|
186
187
|
description:
|
|
187
|
-
"Board context pack:
|
|
188
|
+
"Board/project context pack: board metadata, agent_instructions, journal entries (board_entries), and deployment paths. Complements the lean board_get_ticket_context. Resolves board from board_id / board_slug / board, else the MCP session board. Set include_struck true to include struck journal rows (default false). Same payload as GET /api/boards/:id/context.",
|
|
188
189
|
inputSchema: {
|
|
189
190
|
type: "object",
|
|
190
191
|
properties: {
|
|
@@ -242,18 +243,64 @@ const TOOL_DEFS = [
|
|
|
242
243
|
{
|
|
243
244
|
name: "board_get_ticket_context",
|
|
244
245
|
description:
|
|
245
|
-
"
|
|
246
|
+
"Return the default lean context pack for a ticket: ticket fields, board identity only, relations, blockers, parent/children, and related ticket summaries. Project manual/journal and comments are intentionally omitted; use board_context for board manual/journal and board_read_comments for comment threads.",
|
|
246
247
|
inputSchema: {
|
|
247
248
|
type: "object",
|
|
248
249
|
properties: {
|
|
249
250
|
ticket_id: { type: "string" },
|
|
250
|
-
|
|
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_ticket_context_full",
|
|
266
|
+
description:
|
|
267
|
+
"Return the explicit heavy ticket context pack, preserving the historical board_get_ticket_context shape: ticket, full board row, board_manual/journal, relations, blockers, parent/children, and related tickets. Comments are still intentionally omitted; use board_read_comments for comment threads.",
|
|
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
|
+
depth: { type: "integer", minimum: 1, maximum: 3 },
|
|
276
|
+
max_chars_per_field: { type: "integer", minimum: 1 },
|
|
277
|
+
include_parent_full: { type: "boolean" },
|
|
278
|
+
include_related_full: { type: "boolean" }
|
|
279
|
+
},
|
|
280
|
+
required: ["ticket_id"],
|
|
281
|
+
additionalProperties: false
|
|
282
|
+
},
|
|
283
|
+
handler: (args) => orbitClient.getTicketContextFull(args)
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "board_get_agent_dispatch_packet",
|
|
287
|
+
description:
|
|
288
|
+
"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.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
ticket_id: { type: "string" },
|
|
293
|
+
board_id: { type: "string" },
|
|
294
|
+
board_slug: { type: "string" },
|
|
295
|
+
board: { type: "string" },
|
|
296
|
+
max_chars_per_field: { type: "integer", minimum: 1 },
|
|
297
|
+
comment_limit: { type: "integer", minimum: 0, maximum: 20 }
|
|
298
|
+
},
|
|
299
|
+
required: ["ticket_id"],
|
|
300
|
+
additionalProperties: false
|
|
301
|
+
},
|
|
302
|
+
handler: (args) => orbitClient.getAgentDispatchPacket(args)
|
|
303
|
+
},
|
|
257
304
|
{
|
|
258
305
|
name: "board_read_ticket",
|
|
259
306
|
description:
|
|
@@ -263,7 +310,10 @@ const TOOL_DEFS = [
|
|
|
263
310
|
properties: {
|
|
264
311
|
ticket_id: { type: "string" },
|
|
265
312
|
number: { type: "integer" },
|
|
266
|
-
title: { type: "string" }
|
|
313
|
+
title: { type: "string" },
|
|
314
|
+
board_id: { type: "string" },
|
|
315
|
+
board_slug: { type: "string" },
|
|
316
|
+
board: { type: "string" }
|
|
267
317
|
},
|
|
268
318
|
additionalProperties: false
|
|
269
319
|
},
|
|
@@ -278,7 +328,10 @@ const TOOL_DEFS = [
|
|
|
278
328
|
properties: {
|
|
279
329
|
ticket_id: { type: "string" },
|
|
280
330
|
number: { type: "integer" },
|
|
281
|
-
title: { type: "string" }
|
|
331
|
+
title: { type: "string" },
|
|
332
|
+
board_id: { type: "string" },
|
|
333
|
+
board_slug: { type: "string" },
|
|
334
|
+
board: { type: "string" }
|
|
282
335
|
},
|
|
283
336
|
additionalProperties: false
|
|
284
337
|
},
|
|
@@ -290,7 +343,10 @@ const TOOL_DEFS = [
|
|
|
290
343
|
inputSchema: {
|
|
291
344
|
type: "object",
|
|
292
345
|
properties: {
|
|
293
|
-
ticket_id: { type: "string" }
|
|
346
|
+
ticket_id: { type: "string" },
|
|
347
|
+
board_id: { type: "string" },
|
|
348
|
+
board_slug: { type: "string" },
|
|
349
|
+
board: { type: "string" }
|
|
294
350
|
},
|
|
295
351
|
required: ["ticket_id"],
|
|
296
352
|
additionalProperties: false
|
|
@@ -303,7 +359,10 @@ const TOOL_DEFS = [
|
|
|
303
359
|
inputSchema: {
|
|
304
360
|
type: "object",
|
|
305
361
|
properties: {
|
|
306
|
-
ticket_id: { type: "string" }
|
|
362
|
+
ticket_id: { type: "string" },
|
|
363
|
+
board_id: { type: "string" },
|
|
364
|
+
board_slug: { type: "string" },
|
|
365
|
+
board: { type: "string" }
|
|
307
366
|
},
|
|
308
367
|
required: ["ticket_id"],
|
|
309
368
|
additionalProperties: false
|
|
@@ -321,7 +380,11 @@ const TOOL_DEFS = [
|
|
|
321
380
|
board: { type: "string" },
|
|
322
381
|
board_slug: { type: "string" },
|
|
323
382
|
board_id: { type: "string" },
|
|
324
|
-
limit: { type: "integer", minimum: 1, maximum: 50 }
|
|
383
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
384
|
+
mode: { type: "string", enum: ["ids", "summary", "full"] },
|
|
385
|
+
include_full: { type: "boolean" },
|
|
386
|
+
fields: { type: "array", items: { type: "string" } },
|
|
387
|
+
max_chars_per_field: { type: "integer", minimum: 1 }
|
|
325
388
|
},
|
|
326
389
|
required: ["q"],
|
|
327
390
|
additionalProperties: false
|