@starlove/orbit 1.0.4 → 1.0.6

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 (90) hide show
  1. package/.dockerignore +0 -0
  2. package/Dockerfile +0 -0
  3. package/LICENSE +661 -0
  4. package/README.md +8 -0
  5. package/SKILL-ORBIT.md +0 -0
  6. package/build.mjs +0 -0
  7. package/dist/full/apple-touch-icon.png +0 -0
  8. package/dist/full/cursors/live_arrow.cur +0 -0
  9. package/dist/full/cursors/live_link.cur +0 -0
  10. package/dist/full/cursors/live_move.cur +0 -0
  11. package/dist/full/cursors/live_select.cur +0 -0
  12. package/dist/full/cursors/live_unavail.cur +0 -0
  13. package/dist/full/favicon-32x32.png +0 -0
  14. package/dist/full/index.html +1 -1
  15. package/dist/full/js/{app.7e25f59b.js → app.aedc1bfe.js} +1 -1
  16. package/dist/full/js/app.aedc1bfe.js.map +7 -0
  17. package/dist/full/orbits-favicon.png +0 -0
  18. package/dist/full/orbits-logo-bg.png +0 -0
  19. package/dist/full/orbits-logo.png +0 -0
  20. package/dist/full/styles.c2e3d20a.css +0 -0
  21. package/dist/full/version.json +2 -2
  22. package/docs/AGENT_PROTOCOL.md +0 -0
  23. package/docs/MCP_SETUP.md +0 -0
  24. package/docs/SS-Orbit-Dark.png +0 -0
  25. package/docs/SS-Orbit-Light.png +0 -0
  26. package/docs/mcp-client.example.json +0 -0
  27. package/package.json +2 -2
  28. package/public/apple-touch-icon.png +0 -0
  29. package/public/cursors/live_arrow.cur +0 -0
  30. package/public/cursors/live_link.cur +0 -0
  31. package/public/cursors/live_move.cur +0 -0
  32. package/public/cursors/live_select.cur +0 -0
  33. package/public/cursors/live_unavail.cur +0 -0
  34. package/public/favicon-32x32.png +0 -0
  35. package/public/index.html +0 -0
  36. package/public/js/api.js +0 -0
  37. package/public/js/app.js +0 -0
  38. package/public/js/board-menu.js +0 -0
  39. package/public/js/config.js +0 -0
  40. package/public/js/create-card.js +0 -0
  41. package/public/js/dom.js +0 -0
  42. package/public/js/drawer.js +0 -0
  43. package/public/js/format.js +0 -0
  44. package/public/js/kanban.js +8 -0
  45. package/public/js/local-backend.js +29 -2
  46. package/public/js/main.js +0 -0
  47. package/public/js/modal.js +0 -0
  48. package/public/js/router.js +0 -0
  49. package/public/js/search.js +0 -0
  50. package/public/js/settings-tabs.js +0 -0
  51. package/public/js/settings.js +0 -0
  52. package/public/js/sse.js +0 -0
  53. package/public/js/starscape.js +0 -0
  54. package/public/js/state.js +0 -0
  55. package/public/js/ticket-detail.js +0 -0
  56. package/public/js/toast.js +0 -0
  57. package/public/js/transport.js +0 -0
  58. package/public/js/unread.js +0 -0
  59. package/public/orbits-favicon.png +0 -0
  60. package/public/orbits-logo-bg.png +0 -0
  61. package/public/orbits-logo.png +0 -0
  62. package/public/styles.css +0 -0
  63. package/src/cli/orbit.js +0 -0
  64. package/src/core/agent.js +10 -4
  65. package/src/core/agents-md.js +0 -0
  66. package/src/core/auth.js +0 -0
  67. package/src/core/backups.js +0 -0
  68. package/src/core/board.js +0 -0
  69. package/src/core/boards.js +0 -0
  70. package/src/core/bootstrap.js +1 -1
  71. package/src/core/db.js +0 -0
  72. package/src/core/delete-board.js +0 -0
  73. package/src/core/events.js +0 -0
  74. package/src/core/http.js +0 -0
  75. package/src/core/paths.js +0 -0
  76. package/src/core/provision-repo-board.js +0 -0
  77. package/src/core/queries.js +0 -0
  78. package/src/core/registry.js +0 -0
  79. package/src/core/relations.js +0 -0
  80. package/src/core/router.js +0 -0
  81. package/src/core/search.js +7 -2
  82. package/src/core/seed.js +0 -0
  83. package/src/core/snapshots.js +0 -0
  84. package/src/core/states.js +0 -0
  85. package/src/core/system-picker.js +0 -0
  86. package/src/core/tickets.js +28 -3
  87. package/src/core/util.js +0 -0
  88. package/src/mcp-server.js +0 -0
  89. package/src/server.js +0 -0
  90. package/dist/full/js/app.7e25f59b.js.map +0 -7
File without changes
File without changes
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "edition": "full",
5
- "builtAt": "2026-05-19T06:06:41.796Z"
5
+ "builtAt": "2026-05-20T07:25:07.448Z"
6
6
  }
File without changes
package/docs/MCP_SETUP.md CHANGED
File without changes
Binary file
Binary file
File without changes
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@starlove/orbit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Starscape Orbit — SQLite-backed, agent-first task board (Starscape suite).",
5
- "license": "MIT",
5
+ "license": "AGPL-3.0",
6
6
  "type": "module",
7
7
  "packageManager": "pnpm@10.20.0",
8
8
  "bin": {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/index.html CHANGED
File without changes
package/public/js/api.js CHANGED
File without changes
package/public/js/app.js CHANGED
File without changes
File without changes
File without changes
File without changes
package/public/js/dom.js CHANGED
File without changes
File without changes
File without changes
@@ -257,6 +257,14 @@ function renderColumnBody(columnTickets, allTickets) {
257
257
  continue;
258
258
  }
259
259
 
260
+ // Defer children whose epic is in this column — they'll render under the
261
+ // epic when the loop reaches it. Without this skip, a child iterated before
262
+ // its epic would render standalone here, then again inside epic-children
263
+ // (rendered.add happens at the epic, which may come later in updated_at order).
264
+ if (t.parent_ticket_id && epicIdsInColumn.has(t.parent_ticket_id)) {
265
+ continue;
266
+ }
267
+
260
268
  if (t.parent_ticket_id && epicById.has(t.parent_ticket_id) && !epicIdsInColumn.has(t.parent_ticket_id)) {
261
269
  const epic = epicById.get(t.parent_ticket_id);
262
270
  const siblings = childrenByEpicId.get(t.parent_ticket_id) || [];
@@ -307,7 +307,11 @@ function decorateTickets(snapshot, includeArchived = false) {
307
307
  const labelById = Object.fromEntries(labels.map((l) => [l.id, l]));
308
308
  const stateById = Object.fromEntries(states.map((s) => [s.id, s]));
309
309
  const visible = tickets.filter((t) => includeArchived || !t.archived_at);
310
- const sorted = [...visible].sort((a, b) => String(b.updated_at).localeCompare(String(a.updated_at)));
310
+ const sorted = [...visible].sort((a, b) => {
311
+ const byTime = String(b.updated_at).localeCompare(String(a.updated_at));
312
+ if (byTime !== 0) return byTime;
313
+ return String(a.id).localeCompare(String(b.id));
314
+ });
311
315
  return sorted.map((ticket) => {
312
316
  const stateRow = stateById[ticket.state_id];
313
317
  const tLabels = ticketLabels
@@ -575,6 +579,17 @@ async function handleStateReorder(boardId, body) {
575
579
  });
576
580
  }
577
581
 
582
+ // Mirrors src/core/tickets.js#bumpTicketUpdatedAt — rolls child activity up to
583
+ // the parent epic so the column sort (updated_at DESC) keeps the epic group
584
+ // near recent child activity even when only its children changed.
585
+ async function bumpTicketUpdatedAt(stores, ticketId, time) {
586
+ if (!ticketId) return;
587
+ const parent = await reqPromise(stores.tickets.get(ticketId));
588
+ if (!parent) return;
589
+ parent.updated_at = time;
590
+ stores.tickets.put(parent);
591
+ }
592
+
578
593
  async function handleTicketCreate(body) {
579
594
  const db = await openDb();
580
595
  return putAndEvent(db, async (stores) => {
@@ -610,6 +625,7 @@ async function handleTicketCreate(body) {
610
625
  stores.ticket_labels.put({ key: tlKey(ticket.id, label.id), ticket_id: ticket.id, label_id: label.id });
611
626
  }
612
627
  recordEvent(stores, "ticket_created", ticket.id, { title: ticket.title, type: ticket.type });
628
+ await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
613
629
  return ticket;
614
630
  });
615
631
  }
@@ -620,6 +636,7 @@ async function handleTicketPatch(ticketId, body) {
620
636
  const ticket = await reqPromise(stores.tickets.get(ticketId));
621
637
  if (!ticket) throw err(404, "ticket_not_found");
622
638
  const oldStateId = ticket.state_id;
639
+ const oldParentId = ticket.parent_ticket_id;
623
640
  const SIMPLE = [
624
641
  "title",
625
642
  "description",
@@ -663,6 +680,10 @@ async function handleTicketPatch(ticketId, body) {
663
680
  } else {
664
681
  recordEvent(stores, "ticket_updated", ticketId, { fields: Object.keys(body) });
665
682
  }
683
+ await bumpTicketUpdatedAt(stores, oldParentId, ticket.updated_at);
684
+ if (ticket.parent_ticket_id && ticket.parent_ticket_id !== oldParentId) {
685
+ await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
686
+ }
666
687
  return ticket;
667
688
  });
668
689
  }
@@ -678,6 +699,7 @@ async function handleTicketDelete(ticketId) {
678
699
  const cmts = await getAll(stores.comments, ticketId, "ticket_id");
679
700
  for (const c of cmts) stores.comments.delete(c.id);
680
701
  recordEvent(stores, "ticket_deleted", ticketId, {});
702
+ await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, nowIso());
681
703
  return { ok: true };
682
704
  });
683
705
  }
@@ -691,6 +713,7 @@ async function handleTicketArchive(ticketId, archive) {
691
713
  ticket.updated_at = nowIso();
692
714
  stores.tickets.put(ticket);
693
715
  recordEvent(stores, archive ? "ticket_archived" : "ticket_restored", ticketId, {});
716
+ await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, ticket.updated_at);
694
717
  return ticket;
695
718
  });
696
719
  }
@@ -785,16 +808,20 @@ async function handleCommentCreate(ticketId, body) {
785
808
  return putAndEvent(db, async (stores) => {
786
809
  const ticket = await reqPromise(stores.tickets.get(ticketId));
787
810
  if (!ticket) throw err(404, "ticket_not_found");
811
+ const time = nowIso();
788
812
  const comment = {
789
813
  id: uid(),
790
814
  ticket_id: ticketId,
791
815
  author: "you",
792
816
  kind: body.kind || "note",
793
817
  body: body.body || "",
794
- created_at: nowIso()
818
+ created_at: time
795
819
  };
796
820
  stores.comments.put(comment);
821
+ ticket.updated_at = time;
822
+ stores.tickets.put(ticket);
797
823
  recordEvent(stores, "comment_created", ticketId, { kind: comment.kind });
824
+ await bumpTicketUpdatedAt(stores, ticket.parent_ticket_id, time);
798
825
  return comment;
799
826
  });
800
827
  }
package/public/js/main.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/js/sse.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/styles.css CHANGED
File without changes
package/src/cli/orbit.js CHANGED
File without changes
package/src/core/agent.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  requiredString
25
25
  } from "./util.js";
26
26
  import { boardManual } from "./boards.js";
27
- import { reindexTicket } from "./tickets.js";
27
+ import { bumpTicketUpdatedAt, reindexTicket } from "./tickets.js";
28
28
 
29
29
  /**
30
30
  * Claim the next AI-ready ticket on the resolved board. The router/MCP layer
@@ -71,14 +71,16 @@ export function claimNext(body, ctx) {
71
71
  if (!inProgress) throw httpError(400, "missing_in_progress_state");
72
72
 
73
73
  tx(db, () => {
74
+ const time = now();
74
75
  db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(
75
76
  inProgress.id,
76
- now(),
77
+ time,
77
78
  candidate.id
78
79
  );
79
80
  addComment(db, candidate.id, actor.name, "agent_note", "Agent claimed this ticket and is starting work.");
80
81
  recordEvent(db, candidate.board_id, "agent_claimed", candidate.id, actor.name, { agent_id: actor.id });
81
82
  reindexTicket(db, candidate.id);
83
+ bumpTicketUpdatedAt(db, candidate.parent_ticket_id, time);
82
84
  });
83
85
 
84
86
  return {
@@ -157,7 +159,8 @@ export function checkpointTicket(body, ctx) {
157
159
  if (!review) throw httpError(400, "missing_review_state");
158
160
  const message = requiredString(body.message, "message");
159
161
  tx(db, () => {
160
- db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(review.id, now(), ticket.id);
162
+ const time = now();
163
+ db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(review.id, time, ticket.id);
161
164
  addComment(db, ticket.id, actor.name, "checkpoint", message);
162
165
  recordEvent(db, ticket.board_id, "checkpoint_requested", ticket.id, actor.name, {
163
166
  from: ticket.state_name,
@@ -167,6 +170,7 @@ export function checkpointTicket(body, ctx) {
167
170
  actor_id: actor.id
168
171
  });
169
172
  reindexTicket(db, ticket.id);
173
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
170
174
  });
171
175
  return getContextPack(ticket.id, ctx, 1);
172
176
  }
@@ -190,10 +194,11 @@ export function completeTicket(body, ctx) {
190
194
  if (body.pr_url) lines.push(`PR: ${body.pr_url}`);
191
195
  const updates = body.updates ? appendFieldNote(ticket.implementation_updates, body.updates, actor.name) : ticket.implementation_updates;
192
196
  tx(db, () => {
197
+ const time = now();
193
198
  addComment(db, ticket.id, actor.name, "completion", lines.join("\n\n"));
194
199
  db.prepare(
195
200
  "UPDATE tickets SET state_id = ?, implementation_summary = ?, implementation_updates = ?, updated_at = ? WHERE id = ?"
196
- ).run(nextState.id, summary, updates, now(), ticket.id);
201
+ ).run(nextState.id, summary, updates, time, ticket.id);
197
202
  recordEvent(db, ticket.board_id, "agent_completed", ticket.id, actor.name, {
198
203
  from: ticket.state_name,
199
204
  next_state: nextState.name,
@@ -203,6 +208,7 @@ export function completeTicket(body, ctx) {
203
208
  actor_id: actor.id
204
209
  });
205
210
  reindexTicket(db, ticket.id);
211
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
206
212
  });
207
213
  return getContextPack(ticket.id, ctx, 1);
208
214
  }
File without changes
package/src/core/auth.js CHANGED
File without changes
File without changes
package/src/core/board.js CHANGED
File without changes
File without changes
@@ -105,7 +105,7 @@ export function getBootstrap(actor, requestedBoardId = null) {
105
105
  JOIN states s ON s.id = t.state_id
106
106
  JOIN boards b ON b.id = t.board_id
107
107
  WHERE t.archived_at IS NULL
108
- ORDER BY t.updated_at DESC`
108
+ ORDER BY t.updated_at DESC, t.id ASC`
109
109
  )
110
110
  .all()
111
111
  .filter((ticket) => boardIds.has(ticket.board_id));
package/src/core/db.js CHANGED
File without changes
File without changes
File without changes
package/src/core/http.js CHANGED
File without changes
package/src/core/paths.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -44,13 +44,18 @@ export function searchTickets(args, ctx) {
44
44
  WHERE t.archived_at IS NULL
45
45
  AND (t.title LIKE ? OR t.description LIKE ? OR t.ai_plan LIKE ?
46
46
  OR t.implementation_summary LIKE ? OR t.implementation_updates LIKE ? OR c.body LIKE ?)
47
- ORDER BY t.updated_at DESC
47
+ ORDER BY t.updated_at DESC, t.id ASC
48
48
  LIMIT ?`
49
49
  )
50
50
  .all(like, like, like, like, like, like, limit);
51
51
  }
52
52
 
53
- rows.sort((a, b) => (a.rank !== b.rank ? a.rank - b.rank : String(b.updated_at).localeCompare(String(a.updated_at))));
53
+ rows.sort((a, b) => {
54
+ if (a.rank !== b.rank) return a.rank - b.rank;
55
+ const byTime = String(b.updated_at).localeCompare(String(a.updated_at));
56
+ if (byTime !== 0) return byTime;
57
+ return String(a.id).localeCompare(String(b.id));
58
+ });
54
59
 
55
60
  const seen = new Set();
56
61
  const deduped = [];
package/src/core/seed.js CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -17,6 +17,17 @@ import {
17
17
  requiredString
18
18
  } from "./util.js";
19
19
 
20
+ /**
21
+ * Bump a ticket's `updated_at` without recording an event or touching other
22
+ * fields. Used to roll child activity up to the parent epic so the column sort
23
+ * (ORDER BY updated_at DESC) keeps the epic group near recent activity even
24
+ * when only its children changed. Pass `null`/undefined to no-op.
25
+ */
26
+ export function bumpTicketUpdatedAt(db, ticketId, time) {
27
+ if (!ticketId) return;
28
+ db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
29
+ }
30
+
20
31
  export function createTicket(body, ctx) {
21
32
  const { db, board, actor } = ctx;
22
33
  // The router resolves `ctx.board` from path/body/query before we get here.
@@ -82,6 +93,7 @@ export function createTicket(body, ctx) {
82
93
  parent_ticket_id: parentTicketId
83
94
  });
84
95
  reindexTicket(db, ticketId);
96
+ bumpTicketUpdatedAt(db, parentTicketId, time);
85
97
  return ticketById(db, ticketId);
86
98
  });
87
99
  }
@@ -139,11 +151,12 @@ export function updateTicket(ticketId, body, ctx) {
139
151
  }
140
152
 
141
153
  return tx(db, () => {
154
+ const time = now();
142
155
  if (fields.length > 0) {
143
156
  const sets = fields.map((field) => `${field} = ?`);
144
157
  const values = fields.map((field) => applied[field]);
145
158
  sets.push("updated_at = ?");
146
- values.push(now(), ticketId);
159
+ values.push(time, ticketId);
147
160
  db.prepare(`UPDATE tickets SET ${sets.join(", ")} WHERE id = ?`).run(...values);
148
161
  }
149
162
 
@@ -160,7 +173,7 @@ export function updateTicket(ticketId, body, ctx) {
160
173
  db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticketId, label.id);
161
174
  }
162
175
  if (fields.length === 0) {
163
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(now(), ticketId);
176
+ db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
164
177
  }
165
178
  }
166
179
 
@@ -191,6 +204,13 @@ export function updateTicket(ticketId, body, ctx) {
191
204
  }
192
205
  }
193
206
  reindexTicket(db, ticketId);
207
+ // Roll activity up to the parent epic so the column sort keeps the epic
208
+ // group near recent child activity. On reparent, bump both old and new
209
+ // parent — both views changed (one lost a child, the other gained one).
210
+ bumpTicketUpdatedAt(db, before.parent_ticket_id, time);
211
+ if (after.parent_ticket_id && after.parent_ticket_id !== before.parent_ticket_id) {
212
+ bumpTicketUpdatedAt(db, after.parent_ticket_id, time);
213
+ }
194
214
  return after;
195
215
  });
196
216
  }
@@ -208,6 +228,7 @@ export function archiveTicket(ticketId, ctx) {
208
228
  const time = now();
209
229
  db.prepare("UPDATE tickets SET archived_at = ?, updated_at = ? WHERE id = ?").run(time, time, ticketId);
210
230
  recordEvent(db, ticket.board_id, "ticket_archived", ticketId, actor.name, { title: ticket.title });
231
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
211
232
  return ticketById(db, ticketId);
212
233
  });
213
234
  }
@@ -225,6 +246,7 @@ export function restoreTicket(ticketId, ctx) {
225
246
  const time = now();
226
247
  db.prepare("UPDATE tickets SET archived_at = NULL, updated_at = ? WHERE id = ?").run(time, ticketId);
227
248
  recordEvent(db, ticket.board_id, "ticket_restored", ticketId, actor.name, { title: ticket.title });
249
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
228
250
  return ticketById(db, ticketId);
229
251
  });
230
252
  }
@@ -246,6 +268,7 @@ export function deleteTicket(ticketId, ctx) {
246
268
  });
247
269
  db.prepare("DELETE FROM ticket_fts WHERE ticket_id = ?").run(ticketId);
248
270
  db.prepare("DELETE FROM tickets WHERE id = ?").run(ticketId);
271
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, now());
249
272
  return { ok: true, deleted_id: ticketId };
250
273
  });
251
274
  }
@@ -262,10 +285,12 @@ export function createComment(ticketId, body, ctx) {
262
285
  const kind = body.kind || (actor.type === "agent" ? "agent_note" : "human_comment");
263
286
 
264
287
  return tx(db, () => {
288
+ const time = now();
265
289
  const comment = addComment(db, ticketId, author, kind, commentBody);
266
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(now(), ticketId);
290
+ db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
267
291
  recordEvent(db, ticket.board_id, "comment_created", ticketId, actor.name, { kind: comment.kind });
268
292
  reindexTicket(db, ticketId);
293
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
269
294
  return comment;
270
295
  });
271
296
  }
package/src/core/util.js CHANGED
File without changes
package/src/mcp-server.js CHANGED
File without changes
package/src/server.js CHANGED
File without changes