@starlove/orbit 1.1.1 → 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 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` (heavy), `board_get_ticket_relations`, `board_get_ticket_blockers`, `board_search`, `board_list_archive` |
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` |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "edition": "full",
5
- "builtAt": "2026-05-26T05:55:12.573Z"
5
+ "builtAt": "2026-05-26T06:59:10.275Z"
6
6
  }
@@ -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 heavier ticket pack with relations, blockers, implementation fields, and child cards, call `GET /api/tickets/:id/context` (or MCP `board_get_ticket_context`).
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
- Ticket comments are intentionally omitted from this default agent context. Explicit comment/chat consumers should call:
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`: a trimmed `board` (`name`, `agent_instructions`, `updated_at`), `entries` (journal / decisions / lessons), and `deployment` paths. Use `include_struck=true` to include struck journal rows (default off).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Starscape Orbit ??? zero-dependency, pretty, human-ai-first kanban system.",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",
@@ -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
- return {
779
+ const context = {
780
780
  ticket: { ...ticket, labels },
781
- board: snap.board,
782
- board_manual: { entries: snap.boardEntries.filter((e) => !e.struck_at) },
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
@@ -210,7 +210,18 @@ export function completeTicket(body, ctx) {
210
210
  return getContextPack(ticket.id, ctx, 1);
211
211
  }
212
212
 
213
- export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
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 } = {}) {
214
225
  const { db, board, actor } = ctx;
215
226
  const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
216
227
  const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
@@ -236,13 +247,12 @@ export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
236
247
  ...(parentTicket ? [parentTicket] : [])
237
248
  ]);
238
249
 
239
- return {
250
+ const pack = {
240
251
  ticket: capTicketFields({
241
252
  ...ticket,
242
253
  labels
243
254
  }, maxCharsPerField),
244
- board: innerBoard,
245
- board_manual: boardManual(innerBoard.id, ctx),
255
+ board: includeProjectContext ? innerBoard : contextBoardIdentity(innerBoard, ctx),
246
256
  parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
247
257
  child_tickets: childTickets.map((child) => ({
248
258
  ...capTicketFields(child, maxCharsPerField),
@@ -259,6 +269,17 @@ export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
259
269
  includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
260
270
  )
261
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 });
262
283
  }
263
284
 
264
285
  export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
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, getAgentDispatchPacket, 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
 
@@ -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 { getContextPack } from "./agent.js";
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 = getContextPack(ticket.id, ctx, 1);
441
+ const context = getContextPackFull(ticket.id, ctx, 1);
442
442
  const handoff = buildHandoff({
443
443
  boardRow,
444
444
  context,
@@ -67,6 +67,7 @@ import {
67
67
  completeTicket,
68
68
  getAgentDispatchPacket,
69
69
  getContextPack,
70
+ getContextPackFull,
70
71
  readComments,
71
72
  readTicket
72
73
  } from "./agent.js";
@@ -282,6 +283,11 @@ export async function handleApi(req, res, url) {
282
283
  sendJson(res, 200, getContextPack(ticketId, ctx, depth, agentContextOptions(url)));
283
284
  return;
284
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
+ }
285
291
  if ((sub === "dispatch-packet" || sub === "agent-dispatch-packet") && req.method === "GET") {
286
292
  sendJson(res, 200, getAgentDispatchPacket(ticketId, ctx, agentContextOptions(url)));
287
293
  return;
@@ -87,6 +87,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
87
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" }; },
88
88
  async claimNext(args = {}) { return request("POST", "/api/agent/claim-next", { body: { ...args, board: requireBoard(args) } }); },
89
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 } }); },
90
91
  async getAgentDispatchPacket(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/dispatch-packet`, { query: contextQuery(args) }); },
91
92
  async readTicket(args = {}) {
92
93
  if (args.ticket_id) {
@@ -216,6 +217,7 @@ export async function createLocalOrbitClient(env = process.env) {
216
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 }; },
217
218
  claimNext: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.claimNext(args, ctx)); },
218
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); },
219
221
  getAgentDispatchPacket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.getAgentDispatchPacket(args.ticket_id, ctx, args); },
220
222
  readTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readTicket(args, ctx); },
221
223
  readComments: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.readComments(args, ctx); },
package/src/mcp-server.js CHANGED
@@ -185,7 +185,7 @@ const TOOL_DEFS = [
185
185
  {
186
186
  name: "board_context",
187
187
  description:
188
- "Board context pack: name, agent_instructions, journal entries (board_entries), deployment paths. 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
+ "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.",
189
189
  inputSchema: {
190
190
  type: "object",
191
191
  properties: {
@@ -243,7 +243,7 @@ const TOOL_DEFS = [
243
243
  {
244
244
  name: "board_get_ticket_context",
245
245
  description:
246
- "Read the default agent context pack for a ticket, including board agent_instructions (project-level agent context), relations, blockers, implementation fields, and child cards. Ticket comments are intentionally omitted; use board_read_comments for explicit comment retrieval.",
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.",
247
247
  inputSchema: {
248
248
  type: "object",
249
249
  properties: {
@@ -261,6 +261,27 @@ const TOOL_DEFS = [
261
261
  },
262
262
  handler: (args) => orbitClient.getTicketContext(args)
263
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
+ },
264
285
  {
265
286
  name: "board_get_agent_dispatch_packet",
266
287
  description: