@starlove/orbit 1.0.7 → 1.1.0

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.
Files changed (48) hide show
  1. package/README.md +22 -7
  2. package/SKILL-ORBIT.md +26 -1
  3. package/dist/full/index.html +2 -2
  4. package/dist/full/js/app.a4f045fc.js +676 -0
  5. package/dist/full/js/app.a4f045fc.js.map +7 -0
  6. package/dist/full/{styles.14e9dbbd.css → styles.836872e7.css} +836 -39
  7. package/dist/full/version.json +2 -2
  8. package/docs/AGENT_PROTOCOL.md +23 -5
  9. package/docs/MCP_SETUP.md +5 -3
  10. package/docs/ORBIT_DISPATCH.md +185 -0
  11. package/docs/setup/orbit-init-wizard.md +174 -0
  12. package/package.json +2 -2
  13. package/public/js/actor-labels.js +65 -0
  14. package/public/js/app.js +8 -1
  15. package/public/js/card-actions.js +526 -0
  16. package/public/js/config.js +2 -1
  17. package/public/js/format.js +204 -10
  18. package/public/js/kanban.js +15 -0
  19. package/public/js/local-backend.js +24 -8
  20. package/public/js/router.js +3 -2
  21. package/public/js/search.js +6 -3
  22. package/public/js/settings.js +96 -26
  23. package/public/js/state.js +50 -0
  24. package/public/js/ticket-detail.js +428 -125
  25. package/public/styles.css +836 -39
  26. package/src/cli/orbit.js +217 -32
  27. package/src/core/agent.js +1 -18
  28. package/src/core/agents-md.js +15 -10
  29. package/src/core/attachments.js +238 -0
  30. package/src/core/auth.js +36 -0
  31. package/src/core/backups.js +9 -3
  32. package/src/core/board.js +31 -0
  33. package/src/core/boards.js +1 -1
  34. package/src/core/db.js +42 -2
  35. package/src/core/delete-board.js +87 -21
  36. package/src/core/dispatch.js +532 -0
  37. package/src/core/http.js +31 -11
  38. package/src/core/migrate.js +198 -0
  39. package/src/core/paths.js +6 -0
  40. package/src/core/registry.js +12 -9
  41. package/src/core/router.js +61 -5
  42. package/src/core/search.js +3 -3
  43. package/src/core/snapshots.js +14 -2
  44. package/src/mcp/orbit-client.js +81 -38
  45. package/src/mcp-server.js +190 -6
  46. package/src/server.js +20 -0
  47. package/dist/full/js/app.f4fd5e94.js +0 -604
  48. package/dist/full/js/app.f4fd5e94.js.map +0 -7
package/src/cli/orbit.js CHANGED
@@ -1,36 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Orbit CLI — `orbit init` provisions `.orbit/board.db`, registry row, and
4
- * copies SKILL-ORBIT.md into the target repo. Does not import board.js.
4
+ * copies managed SKILL-ORBIT.md into the target repo. Does not import board.js.
5
5
  */
6
6
 
7
7
  import { spawnSync } from "node:child_process";
8
- import { copyFileSync, existsSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
9
- import { dirname, resolve } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
9
+ import { dirname, join, resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import { removeOrbitAgentsSection, syncAgentsMd } from "../core/agents-md.js";
11
+ import { removeOrbitAgentsSection, syncAgentsMd, syncSkillOrbitMd } from "../core/agents-md.js";
12
12
 
13
13
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
14
14
 
15
15
  function printUsage() {
16
16
  console.log(`Usage:
17
+ orbit -v, --version Print the Orbit CLI version
17
18
  orbit init [options] Provision .orbit/board.db + SKILL-ORBIT.md + AGENTS.md
18
19
  orbit reset [options] Delete .orbit/ + SKILL-ORBIT.md + registry row
19
20
  orbit serve [options] Start the Orbit web app + HTTP API
20
21
  orbit docker [options] Build/run Orbit's web app in Docker
21
22
  orbit mcp [options] Start the stdio MCP server (for agent clients)
23
+ orbit dispatch [options] Dispatch a Hermes profile on a ticket
22
24
 
23
- Options (init / reset / serve / docker / mcp):
25
+ Options (init / reset / serve / docker / mcp / dispatch):
24
26
  --cwd <dir> Project root (default: process.cwd())
25
27
 
26
28
  Options (init only):
27
29
  --no-ai Create the board with AI collaboration disabled
28
30
  --example Create example onboarding tickets
29
- --refresh-agents-md Overwrite SKILL-ORBIT.md and refresh Orbit section in AGENTS.md
30
31
 
31
32
  Options (serve / docker):
32
33
  --port <n> HTTP port (default: 3337, or $PORT)
33
34
 
35
+ Options (dispatch):
36
+ --board <slug-or-id> Local board to dispatch against (default: board for --cwd)
37
+ --ticket <number-id> Ticket number or id to dispatch
38
+ --profile <name> Hermes profile to run (default: agent)
39
+ --policy <name> Autonomous policy wrappers to apply (default: agent-safe; use none to disable)
40
+ --server-url <url> Refused for now; dispatch is local-board only
41
+ --remote Refused for now; use remote MCP/manual orchestration for hosted boards
42
+ --worktree Create and preserve a git worktree for review/testing
43
+ --worktree-path <dir> Worktree path when --worktree is used
44
+ --branch <name> Branch name for the preserved worktree
45
+ --hermes-bin <cmd> Hermes executable (default: hermes)
46
+ --no-spawn Prepare card/worktree/run record but do not start Hermes or move In Progress
47
+ --no-yolo Do not pass --yolo to Hermes (default dispatch passes --yolo)
48
+ --foreground Attach spawned Hermes process to this terminal
49
+ --keep-handoff-file Keep duplicate handoff file under DATA_DIR/dispatch-runs/<board>
50
+ --force Dispatch even when blockers exist
51
+ --dry-run Preview only; no files, worktrees, ticket fields, comments, or agents change
52
+
34
53
  Options (docker only):
35
54
  --image <name> Docker image tag (default: starscape-orbit:local)
36
55
  --data-dir <dir> Docker registry/export data dir (default: <cwd>/.orbit/docker-data)
@@ -41,6 +60,11 @@ Options (docker only):
41
60
  --dry-run Print docker commands without running them`);
42
61
  }
43
62
 
63
+ function printVersion() {
64
+ const packageJson = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8"));
65
+ console.log(packageJson.version);
66
+ }
67
+
44
68
  function shellQuote(value) {
45
69
  return `"${String(value || "").replace(/"/g, '\\"')}"`;
46
70
  }
@@ -59,7 +83,7 @@ async function loadCliCore() {
59
83
  const [
60
84
  { disableAiCollaboration, enableAiCollaboration },
61
85
  { backupBoardDatabase, backupRegistry },
62
- { createRegistrySchema, openConnection },
86
+ { closeConnection, createRegistrySchema, openConnection },
63
87
  { DATA_DIR, ROOT_DIR },
64
88
  { provisionRepoBoard },
65
89
  { deleteBoard, getBoardByRepoPath },
@@ -79,6 +103,7 @@ async function loadCliCore() {
79
103
  ROOT_DIR,
80
104
  backupBoardDatabase,
81
105
  backupRegistry,
106
+ closeConnection,
82
107
  createRegistrySchema,
83
108
  deleteBoard,
84
109
  disableAiCollaboration,
@@ -107,12 +132,36 @@ function disableAiIfRequested(options, registryRow, core) {
107
132
  console.log("AI collaboration disabled for this board.");
108
133
  }
109
134
 
135
+ function isBusyFilesystemError(error) {
136
+ const message = String(error?.message || "").toLowerCase();
137
+ return (
138
+ ["EBUSY", "EPERM", "ENOTEMPTY"].includes(error?.code) ||
139
+ (error?.code === "ERR_SQLITE_ERROR" && (message.includes("locked") || message.includes("busy")))
140
+ );
141
+ }
142
+
143
+ function resetBusyError(error, projectRoot) {
144
+ if (!isBusyFilesystemError(error)) return error;
145
+ const message = [
146
+ `Orbit could not remove local board files under ${projectRoot}.`,
147
+ "",
148
+ "On Windows this usually means an Orbit web server, MCP helper, editor, or terminal still has .orbit/board.db open.",
149
+ "Close Orbit server terminals and restart any AI client with Orbit MCP enabled, then rerun:",
150
+ ` orbit reset --cwd ${shellQuote(projectRoot)}`,
151
+ "",
152
+ `Original error: ${error.message}`
153
+ ].join("\n");
154
+ const wrapped = new Error(message);
155
+ wrapped.code = error.code;
156
+ wrapped.cause = error;
157
+ return wrapped;
158
+ }
159
+
110
160
  function parseArgs(argv) {
111
161
  const args = {
112
162
  command: null,
113
163
  cwd: process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd(),
114
164
  cwdProvided: false,
115
- refreshAgentsMd: false,
116
165
  ai: true,
117
166
  example: false,
118
167
  port: null,
@@ -121,18 +170,43 @@ function parseArgs(argv) {
121
170
  containerName: null,
122
171
  buildImage: true,
123
172
  detach: true,
124
- dryRun: false
173
+ dryRun: false,
174
+ board: null,
175
+ ticket: null,
176
+ profile: "agent",
177
+ policy: "agent-safe",
178
+ serverUrl: null,
179
+ worktree: false,
180
+ worktreePath: null,
181
+ branch: null,
182
+ hermesBin: "hermes",
183
+ noSpawn: false,
184
+ yolo: true,
185
+ keepHandoffFile: false,
186
+ force: false,
187
+ foreground: false,
188
+ remote: false,
189
+ help: false,
190
+ invalid: false
125
191
  };
126
192
  const rest = argv.slice(2);
127
193
  if (rest.length === 0 || rest[0] === "-h" || rest[0] === "--help") {
128
194
  args.command = "help";
129
195
  return args;
130
196
  }
197
+ if (rest[0] === "-v" || rest[0] === "--version") {
198
+ args.command = "version";
199
+ return args;
200
+ }
201
+ if (rest[0] === "dispatch" && (rest[1] === "-h" || rest[1] === "--help")) {
202
+ args.command = "dispatch";
203
+ args.help = true;
204
+ return args;
205
+ }
131
206
  args.command = rest[0];
132
207
  for (let i = 1; i < rest.length; i++) {
133
208
  const a = rest[i];
134
- if (a === "--refresh-agents-md") args.refreshAgentsMd = true;
135
- else if (a === "--no-ai") args.ai = false;
209
+ if (a === "--no-ai") args.ai = false;
136
210
  else if (a === "--example") args.example = true;
137
211
  else if (a === "--cwd" && rest[i + 1]) {
138
212
  args.cwdProvided = true;
@@ -144,6 +218,52 @@ function parseArgs(argv) {
144
218
  args.port = rest[++i];
145
219
  } else if (a.startsWith("--port=")) {
146
220
  args.port = a.slice("--port=".length);
221
+ } else if (a === "--board" && rest[i + 1]) {
222
+ args.board = rest[++i];
223
+ } else if (a.startsWith("--board=")) {
224
+ args.board = a.slice("--board=".length);
225
+ } else if (a === "--ticket" && rest[i + 1]) {
226
+ args.ticket = rest[++i];
227
+ } else if (a.startsWith("--ticket=")) {
228
+ args.ticket = a.slice("--ticket=".length);
229
+ } else if (a === "--profile" && rest[i + 1]) {
230
+ args.profile = rest[++i];
231
+ } else if (a.startsWith("--profile=")) {
232
+ args.profile = a.slice("--profile=".length);
233
+ } else if (a === "--policy" && rest[i + 1]) {
234
+ args.policy = rest[++i];
235
+ } else if (a.startsWith("--policy=")) {
236
+ args.policy = a.slice("--policy=".length);
237
+ } else if (a === "--server-url" && rest[i + 1]) {
238
+ args.serverUrl = rest[++i];
239
+ } else if (a.startsWith("--server-url=")) {
240
+ args.serverUrl = a.slice("--server-url=".length);
241
+ } else if (a === "--remote") {
242
+ args.remote = true;
243
+ } else if (a === "--worktree") {
244
+ args.worktree = true;
245
+ } else if (a === "--worktree-path" && rest[i + 1]) {
246
+ args.worktreePath = resolve(rest[++i]);
247
+ } else if (a.startsWith("--worktree-path=")) {
248
+ args.worktreePath = resolve(a.slice("--worktree-path=".length));
249
+ } else if (a === "--branch" && rest[i + 1]) {
250
+ args.branch = rest[++i];
251
+ } else if (a.startsWith("--branch=")) {
252
+ args.branch = a.slice("--branch=".length);
253
+ } else if (a === "--hermes-bin" && rest[i + 1]) {
254
+ args.hermesBin = rest[++i];
255
+ } else if (a.startsWith("--hermes-bin=")) {
256
+ args.hermesBin = a.slice("--hermes-bin=".length);
257
+ } else if (a === "--no-spawn") {
258
+ args.noSpawn = true;
259
+ } else if (a === "--no-yolo") {
260
+ args.yolo = false;
261
+ } else if (a === "--yolo") {
262
+ args.yolo = true;
263
+ } else if (a === "--keep-handoff-file") {
264
+ args.keepHandoffFile = true;
265
+ } else if (a === "--force") {
266
+ args.force = true;
147
267
  } else if (a === "--image" && rest[i + 1]) {
148
268
  args.image = rest[++i];
149
269
  } else if (a.startsWith("--image=")) {
@@ -162,11 +282,13 @@ function parseArgs(argv) {
162
282
  args.detach = true;
163
283
  } else if (a === "--foreground") {
164
284
  args.detach = false;
285
+ args.foreground = true;
165
286
  } else if (a === "--dry-run") {
166
287
  args.dryRun = true;
167
288
  } else {
168
289
  console.error(`Unknown argument: ${a}`);
169
290
  args.command = "help";
291
+ args.invalid = true;
170
292
  return args;
171
293
  }
172
294
  }
@@ -187,15 +309,13 @@ async function runInit(options) {
187
309
  if (!existsSync(skillSrc)) {
188
310
  console.warn(`Warning: missing ${skillSrc} — skipped copying SKILL-ORBIT.md and AGENTS.md Orbit section.`);
189
311
  } else {
190
- if (skillSrc === skillDest) {
312
+ const skillSync = syncSkillOrbitMd(projectRoot, skillSrc);
313
+ if (skillSync.status === "same_file") {
191
314
  console.log("SKILL-ORBIT.md source is already the project copy — left unchanged.");
192
- } else if (existsSync(skillDest) && !options.refreshAgentsMd) {
193
- console.log(`SKILL-ORBIT.md already exists — left unchanged (use --refresh-agents-md to replace).`);
194
315
  } else {
195
- copyFileSync(skillSrc, skillDest);
196
316
  console.log(`Wrote ${skillDest}`);
197
317
  }
198
- syncAgentsMd(projectRoot, options);
318
+ syncAgentsMd(projectRoot);
199
319
  }
200
320
 
201
321
  enableAiIfEnabled(options, result.registryRow, core);
@@ -204,7 +324,7 @@ async function runInit(options) {
204
324
  if (result.message && result.status === "skipped") {
205
325
  console.log(result.message);
206
326
  } else if (result.status === "created") {
207
- console.log(`Initialized Orbit board at ${resolve(projectRoot, ".orbit", "board.db")}`);
327
+ console.log(`Initialized Orbit board at ${result.registryRow?.db_path ?? "(unknown path)"}`);
208
328
  } else if (result.status === "repaired") {
209
329
  console.log(result.message || "Linked existing board database to the registry.");
210
330
  }
@@ -231,6 +351,7 @@ async function runReset(options) {
231
351
  DATA_DIR,
232
352
  backupBoardDatabase,
233
353
  backupRegistry,
354
+ closeConnection,
234
355
  createRegistrySchema,
235
356
  deleteBoard,
236
357
  getBoardByRepoPath,
@@ -247,23 +368,53 @@ async function runReset(options) {
247
368
  const registryRow = getBoardByRepoPath(projectRoot);
248
369
 
249
370
  if (registryRow && existsSync(registryRow.db_path)) {
250
- backupBoardDatabase(registryRow, openConnection(registryRow.db_path), "pre-cli-reset");
371
+ const db = openConnection(registryRow.db_path);
372
+ try {
373
+ backupBoardDatabase(registryRow, db, "pre-cli-reset");
374
+ } catch (error) {
375
+ throw resetBusyError(error, projectRoot);
376
+ } finally {
377
+ closeConnection(registryRow.db_path);
378
+ }
251
379
  }
252
380
  if (registryRow) backupRegistry("pre-cli-reset");
253
381
 
254
- if (existsSync(orbitDir)) {
255
- rmSync(orbitDir, { recursive: true, force: true });
256
- removed.push(orbitDir);
257
- }
258
- if (existsSync(skillMd)) {
259
- unlinkSync(skillMd);
260
- removed.push(skillMd);
261
- }
262
- const agentsCleanup = removeOrbitAgentsSection(projectRoot);
263
- if (agentsCleanup.removed) {
264
- removed.push(`${agentsCleanup.path} Orbit section`);
265
- } else if (!agentsCleanup.ok) {
266
- console.warn(`Warning: skipped AGENTS.md cleanup (${agentsCleanup.reason}) in ${agentsCleanup.path}`);
382
+ try {
383
+ // Remove the board database. With central storage the db lives outside the
384
+ // project directory; with legacy in-repo storage it is inside orbitDir below.
385
+ const isCentralDb = registryRow && !registryRow.db_path.startsWith(projectRoot + "/");
386
+ if (isCentralDb && existsSync(registryRow.db_path)) {
387
+ const boardDir = dirname(registryRow.db_path);
388
+ rmSync(boardDir, { recursive: true, force: true });
389
+ removed.push(boardDir);
390
+ }
391
+
392
+ // Remove central dispatch-runs for this board.
393
+ if (registryRow) {
394
+ const dispatchRunsDir = join(DATA_DIR, "dispatch-runs", registryRow.slug);
395
+ if (existsSync(dispatchRunsDir)) {
396
+ rmSync(dispatchRunsDir, { recursive: true, force: true });
397
+ removed.push(dispatchRunsDir);
398
+ }
399
+ }
400
+
401
+ // Remove the in-repo .orbit/ directory (covers legacy db, residual dispatch-runs, etc.).
402
+ if (existsSync(orbitDir)) {
403
+ rmSync(orbitDir, { recursive: true, force: true });
404
+ removed.push(orbitDir);
405
+ }
406
+ if (existsSync(skillMd)) {
407
+ unlinkSync(skillMd);
408
+ removed.push(skillMd);
409
+ }
410
+ const agentsCleanup = removeOrbitAgentsSection(projectRoot);
411
+ if (agentsCleanup.removed) {
412
+ removed.push(`${agentsCleanup.path} Orbit section`);
413
+ } else if (!agentsCleanup.ok) {
414
+ console.warn(`Warning: skipped AGENTS.md cleanup (${agentsCleanup.reason}) in ${agentsCleanup.path}`);
415
+ }
416
+ } catch (error) {
417
+ throw resetBusyError(error, projectRoot);
267
418
  }
268
419
 
269
420
  if (registryRow) {
@@ -284,6 +435,7 @@ async function runServe(options) {
284
435
  // captures it at module load. cwd defaults are equivalent for most users, but
285
436
  // an explicit --cwd lets a global install target a board outside cwd.
286
437
  process.env.PROJECT_ROOT = resolve(options.cwd);
438
+ process.env.ORBIT_SYNC_MANAGED_SKILL_ORBIT = "1";
287
439
  if (options.port) process.env.PORT = String(options.port);
288
440
  await import("../server.js");
289
441
  }
@@ -385,6 +537,35 @@ async function runMcp(options) {
385
537
  await import("../mcp-server.js");
386
538
  }
387
539
 
540
+ async function runDispatch(options) {
541
+ if (options.help) {
542
+ printUsage();
543
+ return;
544
+ }
545
+ process.env.PROJECT_ROOT = resolve(options.cwd);
546
+ const { dispatchTicket } = await import("../core/dispatch.js");
547
+ const result = dispatchTicket(options);
548
+ if (result.dry_run) {
549
+ console.log(`Dry run: would dispatch ticket #${result.ticket.number} ${result.ticket.title}`);
550
+ console.log(`Board: ${result.board.slug}`);
551
+ console.log(`Profile: ${result.profile}`);
552
+ console.log(`Mode: ${result.no_spawn ? "prepare-only" : "spawn"}`);
553
+ console.log("No files, worktrees, ticket fields, comments, or agents were changed.");
554
+ return;
555
+ }
556
+ console.log(`Dispatch ${result.spawned ? "started" : "prepared"}: ${result.run_id}`);
557
+ console.log(`Board: ${result.board.slug}`);
558
+ console.log(`Ticket: #${result.ticket.number} ${result.ticket.title}`);
559
+ console.log(`Profile: ${result.profile}`);
560
+ console.log(`Policy: ${result.policy}`);
561
+ console.log(`Worktree: ${result.worktree_path}`);
562
+ if (result.branch) console.log(`Branch: ${result.branch}`);
563
+ if (result.pid) console.log(`PID: ${result.pid}`);
564
+ console.log("AI Written-Plan updated with the generated handoff.");
565
+ console.log("Run record comment added to the ticket.");
566
+ if (result.no_spawn) console.log("No agent spawned; ticket state left unchanged.");
567
+ }
568
+
388
569
  async function main() {
389
570
  const args = parseArgs(process.argv);
390
571
  try {
@@ -398,9 +579,13 @@ async function main() {
398
579
  await runDocker(args);
399
580
  } else if (args.command === "mcp") {
400
581
  await runMcp(args);
582
+ } else if (args.command === "dispatch") {
583
+ await runDispatch(args);
584
+ } else if (args.command === "version") {
585
+ printVersion();
401
586
  } else {
402
587
  printUsage();
403
- process.exitCode = args.command === "help" ? 0 : 1;
588
+ process.exitCode = args.command === "help" && !args.invalid ? 0 : 1;
404
589
  }
405
590
  } catch (err) {
406
591
  console.error(err?.message || err);
package/src/core/agent.js CHANGED
@@ -112,9 +112,6 @@ export function readTicket(lookup, ctx) {
112
112
  const innerBoard = boardById(db, ticket.board_id);
113
113
  requireBoardAccess(actor, innerBoard);
114
114
 
115
- const comments = db
116
- .prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
117
- .all(ticket.id);
118
115
  const labels = labelsForTicket(db, ticket.id);
119
116
 
120
117
  return {
@@ -128,7 +125,6 @@ export function readTicket(lookup, ctx) {
128
125
  priority: ticket.priority,
129
126
  labels
130
127
  },
131
- comments,
132
128
  board_manual: boardManual(innerBoard.id, ctx)
133
129
  };
134
130
  }
@@ -220,7 +216,6 @@ export function getContextPack(ticketId, ctx, depth = 1) {
220
216
  const innerBoard = boardById(db, ticket.board_id);
221
217
  requireBoardAccess(actor, innerBoard);
222
218
 
223
- const comments = db.prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC").all(ticketId);
224
219
  const labels = labelsForTicket(db, ticketId);
225
220
  const relations = relationRows(db, ticketId);
226
221
  const blockers = unresolvedBlockers(db, ticketId);
@@ -236,16 +231,6 @@ export function getContextPack(ticketId, ctx, depth = 1) {
236
231
  ...childTickets,
237
232
  ...(parentTicket ? [parentTicket] : [])
238
233
  ]);
239
- const relatedCommentRows =
240
- depth > 0
241
- ? relatedTickets.flatMap((related) =>
242
- db
243
- .prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT 5")
244
- .all(related.id)
245
- .reverse()
246
- .map((comment) => ({ ...comment, ticket_id: related.id, ticket_title: related.title }))
247
- )
248
- : [];
249
234
 
250
235
  return {
251
236
  ticket: {
@@ -254,7 +239,6 @@ export function getContextPack(ticketId, ctx, depth = 1) {
254
239
  },
255
240
  board: innerBoard,
256
241
  board_manual: boardManual(innerBoard.id, ctx),
257
- comments,
258
242
  parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
259
243
  child_tickets: childTickets.map((child) => ({
260
244
  ...child,
@@ -262,7 +246,6 @@ export function getContextPack(ticketId, ctx, depth = 1) {
262
246
  })),
263
247
  relations,
264
248
  blockers,
265
- related_tickets: relatedTickets,
266
- related_comments: relatedCommentRows
249
+ related_tickets: relatedTickets
267
250
  };
268
251
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
 
4
4
  export const ORBIT_AGENTS_START = "<!-- ORBIT:AGENTS-START -->";
@@ -21,7 +21,19 @@ export function buildAgentsMd() {
21
21
  ${orbitAgentsSection()}`;
22
22
  }
23
23
 
24
- export function syncAgentsMd(projectRoot, options = {}, logger = console.log) {
24
+ export function syncSkillOrbitMd(projectRoot, skillSrc) {
25
+ const skillDest = resolve(projectRoot, "SKILL-ORBIT.md");
26
+ if (!existsSync(skillSrc)) {
27
+ return { ok: false, status: "missing_source", source: skillSrc, path: skillDest };
28
+ }
29
+ if (resolve(skillSrc) === skillDest) {
30
+ return { ok: true, status: "same_file", source: skillSrc, path: skillDest };
31
+ }
32
+ copyFileSync(skillSrc, skillDest);
33
+ return { ok: true, status: "written", source: skillSrc, path: skillDest };
34
+ }
35
+
36
+ export function syncAgentsMd(projectRoot, _options = {}, logger = console.log) {
25
37
  const agentsPath = resolve(projectRoot, "AGENTS.md");
26
38
  const section = orbitAgentsSection();
27
39
 
@@ -36,14 +48,7 @@ export function syncAgentsMd(projectRoot, options = {}, logger = console.log) {
36
48
  const endIndex = current.indexOf(ORBIT_AGENTS_END);
37
49
 
38
50
  if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
39
- if (!options.refreshAgentsMd) {
40
- logger("AGENTS.md already includes Orbit instructions — left unchanged (use --refresh-agents-md to replace).");
41
- return;
42
- }
43
- const before = current.slice(0, startIndex).replace(/\s*$/, "\n\n");
44
- const after = current.slice(endIndex + ORBIT_AGENTS_END.length).replace(/^\s*/, "");
45
- writeFileSync(agentsPath, `${before}${section}${after}`, "utf8");
46
- logger(`Refreshed Orbit instructions inside ${agentsPath}`);
51
+ logger("AGENTS.md already includes Orbit instructions — left unchanged.");
47
52
  return;
48
53
  }
49
54