@starlove/orbit 1.3.5 → 1.4.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 (129) hide show
  1. package/Dockerfile +2 -1
  2. package/README.md +10 -9
  3. package/SKILL-ORBIT.md +41 -17
  4. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  5. package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  6. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  7. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  8. package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  9. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  10. package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  11. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  12. package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  13. package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  14. package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
  15. package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  16. package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  17. package/dist/full/fonts/Inter/OFL.txt +93 -0
  18. package/dist/full/fonts/Inter/README.txt +118 -0
  19. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  20. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  21. package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
  22. package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
  23. package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
  24. package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
  25. package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  26. package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  27. package/dist/full/fonts/Open_Sans/README.txt +100 -0
  28. package/dist/full/fonts/Orbitron/OFL.txt +93 -0
  29. package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  30. package/dist/full/fonts/Orbitron/README.txt +68 -0
  31. package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
  32. package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  33. package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  34. package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  35. package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  36. package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  37. package/dist/full/fonts/runescape-uf.ttf +0 -0
  38. package/dist/full/index.html +135 -25
  39. package/dist/full/js/app.62905fcb.js +952 -0
  40. package/dist/full/js/app.62905fcb.js.map +7 -0
  41. package/dist/full/styles.4e60d824.css +1 -0
  42. package/dist/full/version.json +2 -2
  43. package/docs/AGENT_PROTOCOL.md +20 -13
  44. package/docs/ORBIT_DISPATCH.md +73 -38
  45. package/docs/UPDATING.md +114 -0
  46. package/package.json +5 -7
  47. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  48. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  49. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  50. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  51. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  52. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  53. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  54. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  55. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  56. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  57. package/public/fonts/Chakra_Petch/OFL.txt +93 -0
  58. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  59. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  60. package/public/fonts/Inter/OFL.txt +93 -0
  61. package/public/fonts/Inter/README.txt +118 -0
  62. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  63. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  64. package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
  65. package/public/fonts/JetBrains_Mono/README.txt +79 -0
  66. package/public/fonts/OpenRing-Regular.ttf +0 -0
  67. package/public/fonts/Open_Sans/OFL.txt +93 -0
  68. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  69. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  70. package/public/fonts/Open_Sans/README.txt +100 -0
  71. package/public/fonts/Orbitron/OFL.txt +93 -0
  72. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  73. package/public/fonts/Orbitron/README.txt +68 -0
  74. package/public/fonts/Rajdhani/OFL.txt +93 -0
  75. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  76. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  77. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  78. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  79. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  80. package/public/fonts/runescape-uf.ttf +0 -0
  81. package/public/index.html +133 -23
  82. package/public/js/artifact-drawer.js +79 -0
  83. package/public/js/board-export.js +161 -0
  84. package/public/js/board-menu.js +219 -17
  85. package/public/js/card-actions.js +1 -1
  86. package/public/js/create-card.js +60 -23
  87. package/public/js/drawer.js +164 -4
  88. package/public/js/font-preference.js +229 -0
  89. package/public/js/format.js +0 -10
  90. package/public/js/icons.js +16 -0
  91. package/public/js/local-backend.js +31 -4
  92. package/public/js/main.js +7 -1
  93. package/public/js/modal.js +3 -1
  94. package/public/js/panel.js +58 -0
  95. package/public/js/settings-tabs.js +1 -1
  96. package/public/js/settings.js +606 -275
  97. package/public/js/sse.js +3 -0
  98. package/public/js/state.js +1 -1
  99. package/public/js/text-editing.js +128 -0
  100. package/public/js/ticket-detail.js +377 -148
  101. package/public/js/ticket-typeahead.js +49 -0
  102. package/public/js/toast.js +19 -4
  103. package/public/js/topbar-notes.js +3 -3
  104. package/public/js/typeahead.js +206 -0
  105. package/public/js/update-check.js +58 -0
  106. package/public/styles.css +5447 -3572
  107. package/src/cli/orbit.js +9 -5
  108. package/src/core/agent.js +12 -4
  109. package/src/core/agents-md.js +3 -3
  110. package/src/core/artifacts.js +149 -0
  111. package/src/core/board.js +3 -2
  112. package/src/core/boards.js +0 -1
  113. package/src/core/css-bundle.js +1 -0
  114. package/src/core/db.js +6 -0
  115. package/src/core/dispatch.js +344 -34
  116. package/src/core/http.js +9 -2
  117. package/src/core/paths.js +26 -2
  118. package/src/core/router.js +33 -0
  119. package/src/core/search.js +3 -2
  120. package/src/core/snapshots.js +10 -5
  121. package/src/core/tickets.js +34 -3
  122. package/src/core/util.js +3 -2
  123. package/src/core/version-check.js +154 -0
  124. package/src/mcp/orbit-client.js +18 -2
  125. package/src/mcp-server.js +19 -14
  126. package/src/server.js +28 -3
  127. package/dist/full/js/app.4ae80764.js +0 -742
  128. package/dist/full/js/app.4ae80764.js.map +0 -7
  129. package/dist/full/styles.b9e0c130.css +0 -1
@@ -75,7 +75,7 @@ export function exportBoard(boardId, ctx, options = {}) {
75
75
 
76
76
  return {
77
77
  format: "orbit-board-export",
78
- version: 7,
78
+ version: 9,
79
79
  exported_at: now(),
80
80
  include_attachments: Boolean(options.includeAttachments),
81
81
  board: { ...innerBoard },
@@ -168,9 +168,10 @@ export function importBoardSnapshot(body, ctx) {
168
168
  db.prepare(
169
169
  `INSERT INTO tickets
170
170
  (id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
171
- implementation_summary, implementation_updates, state_id, priority,
171
+ plan_artifact_path, implementation_summary, implementation_artifact_path,
172
+ implementation_updates, state_id, priority,
172
173
  created_by, created_at, updated_at, archived_at)
173
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
174
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
174
175
  ).run(
175
176
  ticket.id,
176
177
  targetBoardId,
@@ -180,7 +181,9 @@ export function importBoardSnapshot(body, ctx) {
180
181
  normalizeTicketType(ticket.type || "task"),
181
182
  ticket.parent_ticket_id || null,
182
183
  ticket.ai_plan || "",
184
+ ticket.plan_artifact_path || "",
183
185
  ticket.implementation_summary || "",
186
+ ticket.implementation_artifact_path || "",
184
187
  ticket.implementation_updates || "",
185
188
  ticket.state_id,
186
189
  normalizePriority(ticket.priority),
@@ -197,13 +200,14 @@ export function importBoardSnapshot(body, ctx) {
197
200
 
198
201
  for (const comment of snapshot.comments || []) {
199
202
  db.prepare(
200
- "INSERT INTO comments (id, ticket_id, author, kind, body, created_at) VALUES (?, ?, ?, ?, ?, ?)"
203
+ "INSERT INTO comments (id, ticket_id, author, kind, body, omitted_for_ai, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
201
204
  ).run(
202
205
  comment.id,
203
206
  comment.ticket_id,
204
207
  comment.author || "import",
205
208
  comment.kind || "comment",
206
209
  comment.body || "",
210
+ comment.omitted_for_ai ? 1 : 0,
207
211
  comment.created_at || now()
208
212
  );
209
213
  }
@@ -463,6 +467,7 @@ function trelloExportToOrbitSnapshot(trello, ctx) {
463
467
  author: trelloMemberName(action.memberCreator),
464
468
  kind: "human_comment",
465
469
  body: cleanText(action.data?.text) || "",
470
+ omitted_for_ai: 0,
466
471
  created_at: cleanText(action.date) || cardUpdatedAt
467
472
  });
468
473
  });
@@ -471,7 +476,7 @@ function trelloExportToOrbitSnapshot(trello, ctx) {
471
476
  return {
472
477
  format: "orbit-board-export",
473
478
  source_format: "trello-board-export",
474
- version: 7,
479
+ version: 9,
475
480
  exported_at: time,
476
481
  include_attachments: false,
477
482
  imported_counts: {
@@ -62,8 +62,9 @@ export function createTicket(body, ctx) {
62
62
  db.prepare(
63
63
  `INSERT INTO tickets
64
64
  (id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
65
- implementation_summary, implementation_updates, state_id, priority, created_by, created_at, updated_at)
66
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
65
+ plan_artifact_path, implementation_summary, implementation_artifact_path,
66
+ implementation_updates, state_id, priority, created_by, created_at, updated_at)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
67
68
  ).run(
68
69
  ticketId,
69
70
  boardId,
@@ -73,7 +74,9 @@ export function createTicket(body, ctx) {
73
74
  ticketType,
74
75
  parentTicketId,
75
76
  body.ai_plan || "",
77
+ body.plan_artifact_path || "",
76
78
  body.implementation_summary || "",
79
+ body.implementation_artifact_path || "",
77
80
  body.implementation_updates || "",
78
81
  stateId,
79
82
  normalizePriority(body.priority),
@@ -113,7 +116,9 @@ export function updateTicket(ticketId, body, ctx) {
113
116
  "type",
114
117
  "parent_ticket_id",
115
118
  "ai_plan",
119
+ "plan_artifact_path",
116
120
  "implementation_summary",
121
+ "implementation_artifact_path",
117
122
  "implementation_updates",
118
123
  "state_id",
119
124
  "priority"
@@ -295,6 +300,32 @@ export function createComment(ticketId, body, ctx) {
295
300
  });
296
301
  }
297
302
 
303
+ export function updateCommentAiOmission(ticketId, commentId, body, ctx) {
304
+ const { db, board, actor } = ctx;
305
+ const ticket = ticketById(db, ticketId);
306
+ if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
307
+ requireBoardAccess(actor, boardById(db, ticket.board_id));
308
+ requirePermission(actor, "write");
309
+
310
+ const comment = db.prepare("SELECT * FROM comments WHERE id = ? AND ticket_id = ?").get(commentId, ticketId);
311
+ if (!comment) throw httpError(404, "comment_not_found");
312
+ const omittedForAi = body.omitted_for_ai ?? body.omittedForAi;
313
+ if (typeof omittedForAi !== "boolean") throw httpError(400, "omitted_for_ai_required");
314
+
315
+ return tx(db, () => {
316
+ const time = now();
317
+ db.prepare("UPDATE comments SET omitted_for_ai = ? WHERE id = ?").run(omittedForAi ? 1 : 0, commentId);
318
+ db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
319
+ recordEvent(db, ticket.board_id, "comment_ai_omission_changed", ticketId, actor.name, {
320
+ comment_id: commentId,
321
+ omitted_for_ai: omittedForAi
322
+ });
323
+ reindexTicket(db, ticketId);
324
+ bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
325
+ return db.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
326
+ });
327
+ }
328
+
298
329
  export function getTicketStatusHistory(ticketId, ctx) {
299
330
  const { db, board, actor } = ctx;
300
331
  const ticket = ticketById(db, ticketId);
@@ -342,7 +373,7 @@ export function reindexTicket(db, ticketId) {
342
373
  const ticket = db.prepare("SELECT * FROM tickets WHERE id = ?").get(ticketId);
343
374
  if (!ticket) return;
344
375
  const comments = db
345
- .prepare("SELECT body FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
376
+ .prepare("SELECT body FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at ASC")
346
377
  .all(ticketId)
347
378
  .map((comment) => comment.body)
348
379
  .join("\n");
package/src/core/util.js CHANGED
@@ -87,10 +87,11 @@ export function normalizeStateRole(value) {
87
87
  }
88
88
 
89
89
  export function appendFieldNote(existing, note, actorName) {
90
+ void actorName;
90
91
  const text = String(note || "").trim();
91
92
  if (!text) return existing || "";
92
- const entry = `## ${new Date().toISOString()} - ${actorName}\n\n${text}`;
93
- return [existing, entry].filter(Boolean).join("\n\n");
93
+ const prior = String(existing || "").trim();
94
+ return [prior, text].filter(Boolean).join("\n\n");
94
95
  }
95
96
 
96
97
  export function toFtsQuery(q) {
@@ -0,0 +1,154 @@
1
+ // Update checking. The server reads its own version from package.json and
2
+ // compares it against the `version` field of package.json on the project's
3
+ // GitHub default branch. We hit the GitHub *contents API* (not the raw CDN):
4
+ // it honours conditional requests / no-cache so the answer is authoritative,
5
+ // and because only the server polls — not every browser — GitHub's 60 req/hr
6
+ // unauthenticated limit is a non-issue. Results are cached in-memory for
7
+ // ORBIT_UPDATE_TTL minutes; `force` bypasses the cache for a "Check now".
8
+ //
9
+ // Env:
10
+ // ORBIT_UPDATE_CHECK=0 disable the check entirely (returns enabled:false)
11
+ // ORBIT_UPDATE_TTL=<min> cache lifetime, default 60
12
+ // ORBIT_UPDATE_BRANCH branch to read, default the repo default (master)
13
+ // ORBIT_GITHUB_TOKEN optional PAT (private repos / higher rate limit)
14
+ // ORBIT_SELF_UPDATE=1 enable the gated POST /api/update self-update
15
+ // ORBIT_DOCKER=1 force "docker" deploy mode
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { spawn } from "node:child_process";
20
+ import { ROOT_DIR, DATA_DIR } from "./paths.js";
21
+
22
+ const DEFAULT_OWNER = "brucestarlove";
23
+ const DEFAULT_REPO = "ss-orbit";
24
+ const CHECK_TIMEOUT_MS = 5000;
25
+
26
+ let pkgCache = null;
27
+ function readPackage() {
28
+ if (pkgCache) return pkgCache;
29
+ try {
30
+ pkgCache = JSON.parse(readFileSync(join(ROOT_DIR, "package.json"), "utf8"));
31
+ } catch {
32
+ pkgCache = {};
33
+ }
34
+ return pkgCache;
35
+ }
36
+
37
+ /** Parse owner/repo out of package.json repository.url, falling back to the
38
+ * known Orbit repo so the check still works on odd checkouts. */
39
+ function repoCoordinates() {
40
+ const url = readPackage()?.repository?.url || "";
41
+ const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/i);
42
+ return { owner: match?.[1] || DEFAULT_OWNER, repo: match?.[2] || DEFAULT_REPO };
43
+ }
44
+
45
+ function localVersion() {
46
+ return readPackage()?.version || "0.0.0";
47
+ }
48
+
49
+ function updateBranch() {
50
+ return process.env.ORBIT_UPDATE_BRANCH || "master";
51
+ }
52
+
53
+ /** How this instance is running, which decides the update instructions the
54
+ * client shows. Docker images carry no git tree, so they're rebuild-only. */
55
+ function detectDeployMode() {
56
+ if (process.env.ORBIT_DOCKER === "1" || existsSync("/.dockerenv")) return "docker";
57
+ if (existsSync(join(ROOT_DIR, ".git"))) return "source";
58
+ return "dist";
59
+ }
60
+
61
+ /** Numeric "a.b.c" comparison (ignores any prerelease suffix). Returns true
62
+ * when `latest` is strictly newer than `current`. */
63
+ function isNewer(latest, current) {
64
+ const parse = (v) => String(v).replace(/^v/, "").split(/[.+-]/).map((n) => Number(n) || 0);
65
+ const a = parse(latest);
66
+ const b = parse(current);
67
+ for (let i = 0; i < 3; i += 1) {
68
+ if ((a[i] || 0) > (b[i] || 0)) return true;
69
+ if ((a[i] || 0) < (b[i] || 0)) return false;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ async function fetchLatestVersion() {
75
+ const { owner, repo } = repoCoordinates();
76
+ const branch = updateBranch();
77
+ const headers = {
78
+ Accept: "application/vnd.github.raw+json",
79
+ "User-Agent": "orbit-update-check"
80
+ };
81
+ if (process.env.ORBIT_GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.ORBIT_GITHUB_TOKEN}`;
82
+
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
85
+ try {
86
+ const res = await fetch(
87
+ `https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${encodeURIComponent(branch)}`,
88
+ { headers, signal: controller.signal, cache: "no-store" }
89
+ );
90
+ if (res.status === 403 || res.status === 429) return { error: "rate_limited" };
91
+ if (!res.ok) return { error: `http_${res.status}` };
92
+ const pkg = JSON.parse(await res.text());
93
+ if (!pkg?.version) return { error: "no_version" };
94
+ return { latest: String(pkg.version) };
95
+ } catch (err) {
96
+ return { error: err?.name === "AbortError" ? "timeout" : "unreachable" };
97
+ } finally {
98
+ clearTimeout(timer);
99
+ }
100
+ }
101
+
102
+ let cache = null; // { at: epochMs, payload }
103
+
104
+ function staticPayload(extra) {
105
+ const { owner, repo } = repoCoordinates();
106
+ const branch = updateBranch();
107
+ return {
108
+ current: localVersion(),
109
+ deployMode: detectDeployMode(),
110
+ selfUpdate: process.env.ORBIT_SELF_UPDATE === "1",
111
+ repoUrl: `https://github.com/${owner}/${repo}`,
112
+ changesUrl: `https://github.com/${owner}/${repo}/commits/${branch}`,
113
+ ...extra
114
+ };
115
+ }
116
+
117
+ /** Get current/latest version info, cached for ORBIT_UPDATE_TTL minutes.
118
+ * Never throws — network problems surface as `{ error, updateAvailable:false }`. */
119
+ export async function getVersionInfo({ force = false } = {}) {
120
+ if (process.env.ORBIT_UPDATE_CHECK === "0") {
121
+ return staticPayload({ enabled: false, latest: null, updateAvailable: false, error: null, checkedAt: null });
122
+ }
123
+
124
+ const ttlMs = (Number(process.env.ORBIT_UPDATE_TTL) || 60) * 60 * 1000;
125
+ if (!force && cache && Date.now() - cache.at < ttlMs) return cache.payload;
126
+
127
+ const result = await fetchLatestVersion();
128
+ const current = localVersion();
129
+ const payload = staticPayload({
130
+ enabled: true,
131
+ latest: result.latest ?? null,
132
+ updateAvailable: Boolean(result.latest) && isNewer(result.latest, current),
133
+ error: result.error ?? null,
134
+ checkedAt: new Date().toISOString()
135
+ });
136
+ cache = { at: Date.now(), payload };
137
+ return payload;
138
+ }
139
+
140
+ /** Kick off scripts/update.sh detached and hand back immediately. Only ever
141
+ * reached when ORBIT_SELF_UPDATE=1 (the router gates the route). The script
142
+ * rebuilds and then signals this process so a supervisor restarts it. */
143
+ export function startSelfUpdate() {
144
+ const script = join(ROOT_DIR, "scripts", "update.sh");
145
+ if (!existsSync(script)) return { started: false, error: "script_missing" };
146
+ const child = spawn("bash", [script], {
147
+ cwd: ROOT_DIR,
148
+ detached: true,
149
+ stdio: "ignore",
150
+ env: { ...process.env, ORBIT_REPO_DIR: ROOT_DIR, ORBIT_SERVER_PID: String(process.pid), ORBIT_DATA_DIR: DATA_DIR }
151
+ });
152
+ child.unref();
153
+ return { started: true };
154
+ }
@@ -61,6 +61,13 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
61
61
  const { board_id, board_slug, board, ...rest } = args;
62
62
  return rest;
63
63
  };
64
+ const settingsPatch = (args = {}) => {
65
+ const patch = {};
66
+ for (const field of ["name", "repo_url", "system_path", "default_branch", "agent_instructions"]) {
67
+ if (args[field] !== undefined) patch[field] = args[field];
68
+ }
69
+ return patch;
70
+ };
64
71
  const searchQuery = (args = {}) => ({
65
72
  q: args.q,
66
73
  limit: args.limit,
@@ -119,6 +126,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
119
126
  async search(args = {}) { return request("GET", "/api/search", { query: searchQuery(args) }); },
120
127
  async createTicket(args = {}) { return request("POST", "/api/tickets", { body: { ...args, board: requireBoard(args) }, ok: [201] }); },
121
128
  async updateTicket({ ticket_id, ...patch }) { return request("PATCH", `/api/tickets/${encodeURIComponent(ticket_id)}`, { query: ticketQuery(patch), body: stripBoardSelectors(patch) }); },
129
+ async uploadMarkdownArtifact({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/artifacts/markdown`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
122
130
  async addComment({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/comments`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
123
131
  async createReviewVerdict({ ticket_id, ...body }) { return request("POST", `/api/tickets/${encodeURIComponent(ticket_id)}/review-verdicts`, { query: ticketQuery(body), body: stripBoardSelectors(body), ok: [201] }); },
124
132
  async listReviewVerdicts(args = {}) { return request("GET", `/api/tickets/${encodeURIComponent(args.ticket_id)}/review-verdicts`, { query: { ...ticketQuery(args), limit: args.limit } }); },
@@ -135,7 +143,7 @@ export function createHttpOrbitClient(env = process.env, fetchImpl = globalThis.
135
143
  query: { include_attachments: args.include_attachments || args.include_images ? "true" : undefined }
136
144
  });
137
145
  },
138
- async updateSettings(args = {}) { const { board_id, board_slug, board, ...patch } = args; return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: patch }); }
146
+ async updateSettings(args = {}) { return request("PATCH", `/api/boards/${encodeURIComponent(requireBoard(args))}`, { body: settingsPatch(args) }); }
139
147
  };
140
148
  }
141
149
 
@@ -226,6 +234,13 @@ export async function createLocalOrbitClient(env = process.env) {
226
234
  const { board_id, board_slug, board: _board, ...rest } = args;
227
235
  return rest;
228
236
  };
237
+ const settingsPatch = (args = {}) => {
238
+ const patch = {};
239
+ for (const field of ["name", "repo_url", "system_path", "default_branch", "agent_instructions"]) {
240
+ if (args[field] !== undefined) patch[field] = args[field];
241
+ }
242
+ return patch;
243
+ };
229
244
  const actor = () => board.localAgentActor();
230
245
 
231
246
  return {
@@ -249,6 +264,7 @@ export async function createLocalOrbitClient(env = process.env) {
249
264
  search: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return board.searchTickets(args, ctx); },
250
265
  createTicket: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const body = stripBoardSelectors(args); body.board_id = ctx.board.id; return backup(ctx, board.createTicket(body, ctx)); },
251
266
  updateTicket: ({ ticket_id, ...patch }) => { const ctx = ctxFor(rowOrSession(patch), actor()); return backup(ctx, board.updateTicket(ticket_id, stripBoardSelectors(patch), ctx)); },
267
+ uploadMarkdownArtifact: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return board.storeTicketMarkdownArtifact(ticket_id, stripBoardSelectors(body), ctx); },
252
268
  addComment: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createComment(ticket_id, stripBoardSelectors(body), ctx)); },
253
269
  createReviewVerdict: ({ ticket_id, ...body }) => { const ctx = ctxFor(rowOrSession(body), actor()); return backup(ctx, board.createReviewVerdict(ticket_id, stripBoardSelectors(body), ctx)); },
254
270
  listReviewVerdicts: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return { review_verdicts: board.listReviewVerdicts(args.ticket_id, ctx, args) }; },
@@ -266,6 +282,6 @@ export async function createLocalOrbitClient(env = process.env) {
266
282
  includeAttachments: Boolean(args.include_attachments || args.include_images)
267
283
  });
268
284
  },
269
- updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); const patch = stripBoardSelectors(args); return backup(ctx, board.updateBoard(ctx.board.id, patch, ctx)); }
285
+ updateSettings: (args = {}) => { const ctx = ctxFor(rowOrSession(args), actor()); return backup(ctx, board.updateBoard(ctx.board.id, settingsPatch(args), ctx)); }
270
286
  };
271
287
  }
package/src/mcp-server.js CHANGED
@@ -187,7 +187,7 @@ const TOOL_DEFS = [
187
187
  {
188
188
  name: "board_context",
189
189
  description:
190
- "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 first; the MCP session board is only a fallback convenience. Set include_struck true to include struck journal rows (default false). Same payload as GET /api/boards/:id/context.",
190
+ "Board/project context pack: agent-safe board metadata, agent_instructions, journal entries (board_entries), and deployment paths. Human-only project_notes are intentionally excluded. Complements the lean board_get_ticket_context. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Set include_struck true to include struck journal rows (default false). Same payload as GET /api/boards/:id/context.",
191
191
  inputSchema: {
192
192
  type: "object",
193
193
  properties: {
@@ -287,7 +287,7 @@ const TOOL_DEFS = [
287
287
  {
288
288
  name: "board_get_agent_dispatch_packet",
289
289
  description:
290
- "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. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Parent/related/implementation bodies are intentionally omitted.",
290
+ "Return a lean agent dispatch packet for one ticket: board identity/instructions, capped description/acceptance, human-visible card fields and linked artifact paths, blockers, shallow parent, relevant workflow state IDs, repo path/default branch, recent capped comments, and label names. Resolves board from board_id / board_slug / board first; the MCP session board is only a fallback convenience. Full parent/related bodies and full comments are intentionally omitted.",
291
291
  inputSchema: {
292
292
  type: "object",
293
293
  properties: {
@@ -407,9 +407,11 @@ const TOOL_DEFS = [
407
407
  description: { type: "string" },
408
408
  type: { type: "string", enum: ["epic", "feature", "task", "bug"] },
409
409
  parent_ticket_id: { type: ["string", "null"] },
410
- ai_plan: { type: "string" },
411
- implementation_summary: { type: "string" },
412
- implementation_updates: { type: "string" },
410
+ ai_plan: { type: "string", description: "Existing AI Plan field: terse human-visible plan summary; link full markdown handoffs with plan_artifact_path." },
411
+ plan_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text plan artifact, e.g. docs/plan.md or ./docs/plan.md. Keep AI Plan terse; do not paste full handoffs into ai_plan." },
412
+ implementation_summary: { type: "string", description: "Existing Work Summary field: terse human-visible result summary; link full reports with implementation_artifact_path." },
413
+ implementation_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text implementation report or evidence artifact, e.g. docs/report.md or ./docs/report.md. Keep Work Summary terse." },
414
+ implementation_updates: { type: "string", description: "Revisions field: terse later changes/corrections after initial implementation; omit logs, chronology, and normal completion notes." },
413
415
  state_id: { type: "string" },
414
416
  priority: { type: "integer" },
415
417
  labels: { type: "array", items: { type: "string" } }
@@ -422,7 +424,7 @@ const TOOL_DEFS = [
422
424
  {
423
425
  name: "board_update_ticket",
424
426
  description:
425
- "Update ticket fields such as AI plan, implementation summary, implementation updates, state, type, priority, or labels (full replace: array of label names). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
427
+ "Update ticket fields such as terse AI Plan / Work Summary text, linked artifact paths, Revisions, state, type, priority, or labels (full replace: array of label names). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
426
428
  inputSchema: {
427
429
  type: "object",
428
430
  properties: {
@@ -434,9 +436,11 @@ const TOOL_DEFS = [
434
436
  description: { type: "string" },
435
437
  type: { type: "string" },
436
438
  parent_ticket_id: { type: ["string", "null"] },
437
- ai_plan: { type: "string" },
438
- implementation_summary: { type: "string" },
439
- implementation_updates: { type: "string" },
439
+ ai_plan: { type: "string", description: "Existing AI Plan field: terse human-visible plan summary; link full markdown handoffs with plan_artifact_path." },
440
+ plan_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text plan artifact, e.g. docs/plan.md or ./docs/plan.md. Keep AI Plan terse; do not paste full handoffs into ai_plan." },
441
+ implementation_summary: { type: "string", description: "Existing Work Summary field: terse human-visible result summary; link full reports with implementation_artifact_path." },
442
+ implementation_artifact_path: { type: "string", description: "Human-editable link/path to a full markdown/text implementation report or evidence artifact, e.g. docs/report.md or ./docs/report.md. Keep Work Summary terse." },
443
+ implementation_updates: { type: "string", description: "Revisions field: terse later changes/corrections after initial implementation; omit logs, chronology, and normal completion notes." },
440
444
  state_id: { type: "string" },
441
445
  priority: { type: "integer" },
442
446
  labels: { type: "array", items: { type: "string" }, description: "Replaces all ticket labels when provided." }
@@ -562,7 +566,7 @@ const TOOL_DEFS = [
562
566
  {
563
567
  name: "board_complete",
564
568
  description:
565
- "Hand off finished work for human review: moves the ticket to the Review lane (by role or name), writes a completion comment (summary, optional PR URL), and optional implementation_updates. This is the usual 'ready for you to look' path; use board_checkpoint only when blocked mid-flight. Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
569
+ "Hand off finished work for human review: moves the ticket to the Review lane (by role or name), writes a compact completion comment, stores summary as the terse Work Summary (`implementation_summary`), and accepts optional linked implementation_artifact_path. Pass updates only for Revisions: terse later changes/corrections after initial implementation, not normal completion notes. This is the usual 'ready for you to look' path; use board_checkpoint only when blocked mid-flight. Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
566
570
  inputSchema: {
567
571
  type: "object",
568
572
  properties: {
@@ -570,8 +574,10 @@ const TOOL_DEFS = [
570
574
  board_id: { type: "string" },
571
575
  board_slug: { type: "string" },
572
576
  board: { type: "string" },
573
- summary: { type: "string" },
574
- updates: { type: "string" },
577
+ summary: { type: "string", description: "Terse human-visible Work Summary stored in implementation_summary." },
578
+ updates: { type: "string", description: "Optional Revisions text only: terse later changes/corrections after initial implementation." },
579
+ implementation_artifact_path: { type: "string", description: "Human-editable link/path to a markdown/text implementation report artifact; can point at a report generated outside Orbit." },
580
+ summary_artifact_path: { type: "string", description: "Compatibility alias for implementation_artifact_path." },
575
581
  pr_url: { type: "string" },
576
582
  next_state: { type: "string" }
577
583
  },
@@ -647,7 +653,7 @@ const TOOL_DEFS = [
647
653
  {
648
654
  name: "board_update_settings",
649
655
  description:
650
- "PATCH-equivalent for board settings: name (display rename only; slug/canonical URLs stay unchanged), repo_url, system_path, default_branch, agent_instructions (project-level agent context), and project_notes (Notes For You). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
656
+ "PATCH-equivalent for board settings: name (display rename only; slug/canonical URLs stay unchanged), repo_url, system_path, default_branch, and agent_instructions (project-level agent context). Resolves board from board_id / board_slug / board; the MCP session board is only a fallback convenience.",
651
657
  inputSchema: {
652
658
  type: "object",
653
659
  properties: {
@@ -658,7 +664,6 @@ const TOOL_DEFS = [
658
664
  repo_url: { type: "string" },
659
665
  system_path: { type: "string" },
660
666
  default_branch: { type: "string" },
661
- project_notes: { type: "string" },
662
667
  agent_instructions: { type: "string" }
663
668
  },
664
669
  additionalProperties: false
package/src/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:http";
2
+ import { networkInterfaces } from "node:os";
2
3
  import {
3
4
  boardRuntime,
4
5
  handleApi,
@@ -41,15 +42,36 @@ const server = createServer(async (req, res) => {
41
42
  });
42
43
 
43
44
  const requestedPort = boardRuntime.port;
45
+ const requestedHost = process.env.HOST || process.env.ORBIT_HOST || "";
44
46
  const hasExplicitPort = Boolean(process.env.PORT);
45
47
  const maxAutoPort = requestedPort + 99;
46
48
  let currentPort = requestedPort;
47
49
 
50
+ function listen(port) {
51
+ if (requestedHost) {
52
+ server.listen(port, requestedHost);
53
+ return;
54
+ }
55
+ server.listen(port);
56
+ }
57
+
58
+ function displayHost(host) {
59
+ if (!host || host === "::" || host === "0.0.0.0") return "localhost";
60
+ return host;
61
+ }
62
+
63
+ function networkUrls(port) {
64
+ return Object.values(networkInterfaces())
65
+ .flat()
66
+ .filter((item) => item && item.family === "IPv4" && !item.internal)
67
+ .map((item) => `http://${item.address}:${port}`);
68
+ }
69
+
48
70
  server.on("error", (error) => {
49
71
  if (!hasExplicitPort && error?.code === "EADDRINUSE" && currentPort < maxAutoPort) {
50
72
  currentPort += 1;
51
73
  console.warn(`[orbit] Port ${currentPort - 1} is in use; trying ${currentPort}.`);
52
- server.listen(currentPort);
74
+ listen(currentPort);
53
75
  return;
54
76
  }
55
77
 
@@ -58,13 +80,16 @@ server.on("error", (error) => {
58
80
  process.exit(1);
59
81
  });
60
82
 
61
- server.listen(currentPort);
83
+ listen(currentPort);
62
84
 
63
85
  server.on("listening", () => {
64
86
  const address = server.address();
65
87
  const port = typeof address === "object" && address ? address.port : currentPort;
66
- const summary = startupSummary(port);
88
+ const host = typeof address === "object" && address ? displayHost(address.address) : displayHost(requestedHost);
89
+ const summary = startupSummary(port, host);
67
90
  console.log(`Starscape Orbit listening on ${summary.url}`);
91
+ const urls = networkUrls(port).filter((url) => url !== summary.url);
92
+ if (urls.length) console.log(`Network: ${urls.join(", ")}`);
68
93
  console.log(`Registry: ${summary.registryPath}`);
69
94
  console.log(`Boards (${summary.boardCount}):`);
70
95
  for (const path of summary.boardPaths) console.log(` - ${path}`);