@starlove/orbit 1.4.0 → 2.0.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 (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.62905fcb.js +0 -952
  204. package/dist/full/js/app.62905fcb.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -229
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -0,0 +1,263 @@
1
+ /**
2
+ * The records Orbit keeps, typed with the names the JSON API, the export
3
+ * format (`.orbit.json`) and the database already use: one name per field
4
+ * everywhere, snake_case, so nothing maps between layers.
5
+ *
6
+ * Each record is what storage holds, field for field, in the order answers list
7
+ * them. Flags are 0 or 1 and JSON columns stay strings, because answers and
8
+ * exports carry them that way. What the core derives for an answer (a card's
9
+ * lane name, its comment count) is built from these and is not stored.
10
+ *
11
+ * This file has no logic but the one helper every adapter needs to turn a
12
+ * stored event into the change it announces.
13
+ */
14
+ /** A record's id: a UUID for anything Orbit made, any string an import brought. */
15
+ export type Id = string;
16
+ /** An instant as `Date.prototype.toISOString()` writes it (UTC, milliseconds). */
17
+ export type Timestamp = string;
18
+ /** A yes or no as storage and answers carry it. */
19
+ export type Flag = 0 | 1;
20
+ /** 0 to 4. People and agents read them as words: maybe, low, med, high, urgent. */
21
+ export type Priority = 0 | 1 | 2 | 3 | 4;
22
+ export type TicketType = "epic" | "feature" | "task" | "bug";
23
+ /** What a lane does in the agent flow. At most one lane per board holds each role. */
24
+ export type LaneRole = "ai_ready" | "in_progress" | "review" | "done";
25
+ /** A journal entry's kind. */
26
+ export type EntryType = "decision" | "lesson";
27
+ export type Verdict = "PASS" | "BLOCK" | "QUESTION";
28
+ /** A board's own record: what its settings hold. */
29
+ export interface Board {
30
+ id: Id;
31
+ slug: string;
32
+ name: string;
33
+ repo_url: string;
34
+ default_branch: string;
35
+ project_notes: string;
36
+ agent_instructions: string;
37
+ ai_enabled: Flag;
38
+ created_at: Timestamp;
39
+ updated_at: Timestamp;
40
+ }
41
+ /**
42
+ * A board as the list of boards shows it. `db_path` says where the adapter
43
+ * keeps the board's data, for people to read (a file for SQLite); adapters
44
+ * without files leave it empty. Nothing parses it.
45
+ */
46
+ export interface RegisteredBoard {
47
+ id: Id;
48
+ slug: string;
49
+ name: string;
50
+ db_path: string;
51
+ repo_url: string;
52
+ default_branch: string;
53
+ last_active_at: Timestamp | null;
54
+ created_at: Timestamp;
55
+ updated_at: Timestamp;
56
+ }
57
+ /** A lane. The code calls lanes states, as the API does. */
58
+ export interface State {
59
+ id: Id;
60
+ board_id: Id;
61
+ name: string;
62
+ position: number;
63
+ is_default: Flag;
64
+ role: LaneRole | null;
65
+ created_at: Timestamp;
66
+ }
67
+ /** A card. The code calls cards tickets, as the API does. */
68
+ export interface Ticket {
69
+ id: Id;
70
+ board_id: Id;
71
+ /** Unique on its board, assigned by storage when the card is made. */
72
+ number: number;
73
+ title: string;
74
+ description: string;
75
+ type: TicketType;
76
+ /** An epic on the same board. */
77
+ parent_ticket_id: Id | null;
78
+ ai_plan: string;
79
+ plan_artifact_path: string;
80
+ implementation_summary: string;
81
+ implementation_artifact_path: string;
82
+ implementation_updates: string;
83
+ /** A link to a Nebula file or folder, or ''. */
84
+ nebula_url: string;
85
+ state_id: Id;
86
+ priority: Priority;
87
+ /** The actor's name. */
88
+ created_by: string;
89
+ created_at: Timestamp;
90
+ /** Every change to the card moves it, a sub-card's change moves its epic's too. */
91
+ updated_at: Timestamp;
92
+ archived_at: Timestamp | null;
93
+ }
94
+ export interface Label {
95
+ id: Id;
96
+ board_id: Id;
97
+ /** Unique on its board. */
98
+ name: string;
99
+ color: string;
100
+ created_at: Timestamp;
101
+ }
102
+ export interface TicketLabel {
103
+ ticket_id: Id;
104
+ label_id: Id;
105
+ }
106
+ /**
107
+ * A comment on a card. `kind` is free text: Orbit writes human_comment,
108
+ * agent_note, checkpoint and completion, and an import may bring others.
109
+ */
110
+ export interface Comment {
111
+ id: Id;
112
+ ticket_id: Id;
113
+ author: string;
114
+ kind: string;
115
+ body: string;
116
+ /** 1: hidden from agents. Kept out of everything an agent can read or search. */
117
+ omitted_for_ai: Flag;
118
+ created_at: Timestamp;
119
+ }
120
+ /**
121
+ * A link between two cards on one board. Orbit writes blocks, blocked_by and
122
+ * relates_to; the JSON API stores any type it is sent.
123
+ */
124
+ export interface Relation {
125
+ id: Id;
126
+ source_ticket_id: Id;
127
+ target_ticket_id: Id;
128
+ type: string;
129
+ created_at: Timestamp;
130
+ }
131
+ /**
132
+ * A link from a card on this board to a card on another board of the same
133
+ * Orbit. It lives on the source card's board and names the target by board
134
+ * and id; nothing on the target's board changes. Whether a cross-board blocker
135
+ * still blocks is read from the target card when asked, so nothing has to
136
+ * clean it up when that card is done.
137
+ */
138
+ export interface CrossBoardRelation {
139
+ id: Id;
140
+ source_ticket_id: Id;
141
+ target_board_id: Id;
142
+ target_ticket_id: Id;
143
+ type: "blocked_by" | "relates_to";
144
+ created_at: Timestamp;
145
+ }
146
+ /** A journal entry: a decision or a lesson, optionally about a card. A struck entry stays, out of agents' context, until a person deletes it. */
147
+ export interface BoardEntry {
148
+ id: Id;
149
+ board_id: Id;
150
+ type: EntryType;
151
+ title: string;
152
+ body: string;
153
+ ticket_id: Id | null;
154
+ created_by: string;
155
+ created_at: Timestamp;
156
+ updated_at: Timestamp;
157
+ struck_at: Timestamp | null;
158
+ }
159
+ /** A reviewer's verdict on a card. The `_json` fields hold JSON arrays as text. */
160
+ export interface ReviewVerdict {
161
+ id: Id;
162
+ ticket_id: Id;
163
+ verdict: Verdict;
164
+ blocking_findings_json: string;
165
+ optional_findings_json: string;
166
+ evidence_commands_json: string;
167
+ reviewer_profile: string;
168
+ reviewer_session_id: string;
169
+ reviewed_commit_sha: string;
170
+ dispatch_run_id: string;
171
+ supersedes_prior_review_id: Id | null;
172
+ created_by: string;
173
+ created_at: Timestamp;
174
+ }
175
+ /** An image on a card. Its bytes are in the BlobStore at `attachments/<stored_path>`. */
176
+ export interface Attachment {
177
+ id: Id;
178
+ ticket_id: Id;
179
+ board_id: Id;
180
+ original_name: string;
181
+ /** Relative to the board's attachments, POSIX-style: tickets/<ticket id>/<attachment id>.png */
182
+ stored_path: string;
183
+ mime_type: string;
184
+ size_bytes: number;
185
+ sha256: string;
186
+ created_by: string;
187
+ created_at: Timestamp;
188
+ }
189
+ /** What Orbit records as it changes a board. Imports may bring other types. */
190
+ export type EventType = "board_created" | "board_updated" | "board_imported" | "board_entry_created" | "board_entry_struck" | "board_entry_restored" | "board_entry_deleted" | "state_created" | "state_updated" | "states_reordered" | "state_deleted" | "ticket_created" | "ticket_updated" | "state_changed" | "ticket_archived" | "ticket_restored" | "ticket_deleted" | "comment_created" | "comment_ai_omission_changed" | "relation_created" | "relation_deleted" | "attachment_created" | "attachment_deleted" | "review_verdict_created" | "agent_claimed" | "checkpoint_requested" | "agent_completed";
191
+ /**
192
+ * One entry in a board's history. `body_json` is the event's data as JSON
193
+ * text. Every write that changes a board appends at least one, and the live
194
+ * stream, an agent's `changes` and the quick menu's sessions are read from
195
+ * them.
196
+ */
197
+ export interface OrbitEvent {
198
+ id: Id;
199
+ ticket_id: Id | null;
200
+ /** The actor's name. */
201
+ actor: string;
202
+ type: EventType | (string & {});
203
+ body_json: string;
204
+ created_at: Timestamp;
205
+ }
206
+ /**
207
+ * A committed change, as `BoardStore.watch()` and the live event stream
208
+ * (`GET /api/events/stream`) announce it: the event, with its board and its
209
+ * data parsed.
210
+ */
211
+ export interface BoardChange {
212
+ id: Id;
213
+ type: OrbitEvent["type"];
214
+ ticket_id: Id | null;
215
+ board_id: Id;
216
+ actor: string;
217
+ data: Record<string, unknown>;
218
+ timestamp: Timestamp;
219
+ }
220
+ /** The change a stored event announces. Unreadable data announces as `{}`. */
221
+ export declare function changeOf(boardId: Id, event: OrbitEvent): BoardChange;
222
+ /**
223
+ * Who is acting. Orbit has no accounts: a person at the computer is `local`,
224
+ * and an agent is named by `ORBIT_AGENT_NAME` or the `x-orbit-agent-name`
225
+ * header. Hosts that add accounts resolve their own actor here.
226
+ */
227
+ export interface Actor {
228
+ id: string;
229
+ name: string;
230
+ type: "human" | "agent";
231
+ role: string;
232
+ permissions: string[];
233
+ /** Board slugs the actor may open; ["*"] for all. */
234
+ boards: string[];
235
+ }
236
+ /**
237
+ * An agent's run of writes on one board, for the quick menu (`GET
238
+ * /api/sessions`). Derived from events; never stored.
239
+ */
240
+ export interface AgentSession {
241
+ /** `<board id>:<agent>:<first event id>` */
242
+ id: string;
243
+ board: {
244
+ id: Id;
245
+ slug: string;
246
+ name: string;
247
+ };
248
+ agent: string;
249
+ /** True when the agent never said its name. */
250
+ unnamed: boolean;
251
+ state: "waiting" | "working" | "finished";
252
+ reason: "ask" | "review" | null;
253
+ what: string;
254
+ detail: string;
255
+ card: {
256
+ id: Id;
257
+ number: number;
258
+ title: string;
259
+ } | null;
260
+ started_at: Timestamp;
261
+ at: Timestamp;
262
+ writes: number;
263
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The records Orbit keeps, typed with the names the JSON API, the export
3
+ * format (`.orbit.json`) and the database already use: one name per field
4
+ * everywhere, snake_case, so nothing maps between layers.
5
+ *
6
+ * Each record is what storage holds, field for field, in the order answers list
7
+ * them. Flags are 0 or 1 and JSON columns stay strings, because answers and
8
+ * exports carry them that way. What the core derives for an answer (a card's
9
+ * lane name, its comment count) is built from these and is not stored.
10
+ *
11
+ * This file has no logic but the one helper every adapter needs to turn a
12
+ * stored event into the change it announces.
13
+ */
14
+ /** The change a stored event announces. Unreadable data announces as `{}`. */
15
+ export function changeOf(boardId, event) {
16
+ return {
17
+ id: event.id,
18
+ type: event.type,
19
+ ticket_id: event.ticket_id,
20
+ board_id: boardId,
21
+ actor: event.actor,
22
+ data: parseData(event.body_json),
23
+ timestamp: event.created_at
24
+ };
25
+ }
26
+ function parseData(json) {
27
+ try {
28
+ const value = JSON.parse(json || "{}");
29
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A card's optional link to a Nebula file or folder. Orbit keeps the address
3
+ * people paste when it is a plain web link: http or https, no user name or
4
+ * password, no spaces or control characters, at most 2,048 characters. Any
5
+ * host is allowed, since a Nebula can live on your own server or tailnet as
6
+ * well as at nebula.starscape.app. An empty value clears the link.
7
+ *
8
+ * The web app imports it from here (web/src/card/).
9
+ */
10
+ export declare const NEBULA_URL_MAX = 2048;
11
+ /** The normalized link, "" for none, or null when Orbit wouldn't keep it. */
12
+ export declare function parseNebulaUrl(value: unknown): string | null;
13
+ /** The link to store ("" clears it), or `invalid_nebula_url`. */
14
+ export declare function normalizeNebulaUrl(value: unknown): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A card's optional link to a Nebula file or folder. Orbit keeps the address
3
+ * people paste when it is a plain web link: http or https, no user name or
4
+ * password, no spaces or control characters, at most 2,048 characters. Any
5
+ * host is allowed, since a Nebula can live on your own server or tailnet as
6
+ * well as at nebula.starscape.app. An empty value clears the link.
7
+ *
8
+ * The web app imports it from here (web/src/card/).
9
+ */
10
+ import { OrbitError } from "./errors.js";
11
+ import { asText } from "./text.js";
12
+ export const NEBULA_URL_MAX = 2048;
13
+ /** The normalized link, "" for none, or null when Orbit wouldn't keep it. */
14
+ export function parseNebulaUrl(value) {
15
+ const text = asText(value ?? "").trim();
16
+ if (!text)
17
+ return "";
18
+ if (text.length > NEBULA_URL_MAX || /[\s\u0000-\u001f\u007f]/.test(text))
19
+ return null;
20
+ let url;
21
+ try {
22
+ url = new URL(text);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ if (url.protocol !== "https:" && url.protocol !== "http:")
28
+ return null;
29
+ if (url.username || url.password || !url.hostname)
30
+ return null;
31
+ return url.href.length > NEBULA_URL_MAX ? null : url.href;
32
+ }
33
+ /** The link to store ("" clears it), or `invalid_nebula_url`. */
34
+ export function normalizeNebulaUrl(value) {
35
+ const kept = parseNebulaUrl(value);
36
+ if (kept === null)
37
+ throw new OrbitError("invalid_nebula_url");
38
+ return kept;
39
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Links between cards on one board: blocks, blocked_by and relates_to. An
3
+ * epic and its sub-cards are linked by the sub-card's `parent_ticket_id`,
4
+ * which answers show as read-only `child_of` and `parent_of` entries.
5
+ */
6
+ import type { BoardCtx } from "./context.ts";
7
+ import type { CrossCard, CrossLink } from "./cross-board.ts";
8
+ import type { Id, Relation } from "./model.ts";
9
+ /** The relation types Orbit keeps. */
10
+ export declare const RELATION_TYPES: readonly ["blocks", "blocked_by", "relates_to"];
11
+ export declare function createRelation(c: BoardCtx, input: {
12
+ source_ticket_id?: unknown;
13
+ target_ticket_id?: unknown;
14
+ type?: unknown;
15
+ }): Promise<Relation>;
16
+ export declare function deleteRelation(c: BoardCtx, relationId: Id): Promise<{
17
+ ok: true;
18
+ }>;
19
+ /** The other card of a link, as a card's relations and blockers name it. */
20
+ export interface LinkedCard {
21
+ id: Id;
22
+ number: number;
23
+ title: string;
24
+ type: string;
25
+ state_id: Id;
26
+ state_name: string;
27
+ state_role: string | null;
28
+ parent_ticket_id: Id | null;
29
+ }
30
+ /** One link as a card's relations list it. */
31
+ export interface RelationLine {
32
+ id: Id | null;
33
+ type: string;
34
+ direction: string;
35
+ source: "relation" | "hierarchy";
36
+ other_ticket: LinkedCard;
37
+ }
38
+ /** Every link touching a card: its relations, then its epic (`child_of`) and its open sub-cards (`parent_of`). */
39
+ export declare function ticketRelations(c: BoardCtx, ticketId: Id): Promise<TicketRelations>;
40
+ /** A card's links: its relations, its epic and its open sub-cards, and its links to cards on other boards. */
41
+ export interface TicketRelations {
42
+ ticket_id: Id;
43
+ relations: RelationLine[];
44
+ cross_board: CrossLink[];
45
+ }
46
+ /** A card still blocking another; `via_epic_id` when it blocks the card's epic rather than the card. */
47
+ export type Blocker = Omit<LinkedCard, "parent_ticket_id"> & {
48
+ via_epic_id: Id | null;
49
+ };
50
+ /** What still blocks a card, on its board and on others, and whether it can start. */
51
+ export interface TicketBlockers {
52
+ ticket_id: Id;
53
+ can_start: boolean;
54
+ blockers: Blocker[];
55
+ waiting_on: CrossCard[];
56
+ }
57
+ /** What still blocks a card, and whether it can start. */
58
+ export declare function ticketBlockers(c: BoardCtx, ticketId: Id): Promise<TicketBlockers>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Links between cards on one board: blocks, blocked_by and relates_to. An
3
+ * epic and its sub-cards are linked by the sub-card's `parent_ticket_id`,
4
+ * which answers show as read-only `child_of` and `parent_of` entries.
5
+ */
6
+ import { crossBlockersFor, crossLinks } from "./cross-board.js";
7
+ import { OrbitError } from "./errors.js";
8
+ import { record } from "./history.js";
9
+ import { ticketOn } from "./tickets.js";
10
+ import { requiredString } from "./text.js";
11
+ import { Lanes, laned, relationRows, ticketView, unresolvedBlockers } from "./views.js";
12
+ /** The relation types Orbit keeps. */
13
+ export const RELATION_TYPES = ["blocks", "blocked_by", "relates_to"];
14
+ export async function createRelation(c, input) {
15
+ const sourceId = requiredString(input.source_ticket_id, "source_ticket_id");
16
+ const relation = await c.data.write(async (w) => {
17
+ const source = await ticketOn(w, sourceId);
18
+ // A relation lives on one board; a card on another board is no card here.
19
+ const target = await ticketOn(w, requiredString(input.target_ticket_id, "target_ticket_id"));
20
+ if (source.id === target.id)
21
+ throw new OrbitError("relation_self");
22
+ if (source.parent_ticket_id === target.id || target.parent_ticket_id === source.id)
23
+ throw new OrbitError("relation_redundant_with_parent");
24
+ const type = typeof input.type === "string" && input.type ? input.type : "relates_to";
25
+ // Epics and sub-cards are linked by parent_ticket_id, not by relations.
26
+ if (type === "child_of" || type === "parent_of")
27
+ throw new OrbitError("hierarchy_uses_parent_ticket_id");
28
+ if (!RELATION_TYPES.includes(type))
29
+ throw new OrbitError("invalid_relation_type");
30
+ const lanes = await Lanes.of(w);
31
+ // A blocker already done would block nothing.
32
+ if (type === "blocked_by" && lanes.roleOf(target) === "done")
33
+ throw new OrbitError("blocker_target_already_done");
34
+ if (type === "blocks" && lanes.roleOf(source) === "done")
35
+ throw new OrbitError("blocker_source_already_done");
36
+ if ((await w.relations.list(source.id)).some((r) => r.source_ticket_id === source.id && r.target_ticket_id === target.id && r.type === type)) {
37
+ throw new OrbitError("relation_exists");
38
+ }
39
+ const made = { id: c.ids.next(), source_ticket_id: source.id, target_ticket_id: target.id, type, created_at: c.clock.now() };
40
+ await w.relations.insert(made);
41
+ await record(w, c, "relation_created", source.id, { target_ticket_id: target.id, type });
42
+ return made;
43
+ });
44
+ c.changed(c.board.id);
45
+ return relation;
46
+ }
47
+ export async function deleteRelation(c, relationId) {
48
+ await c.data.write(async (w) => {
49
+ const relation = await w.relations.get(relationId);
50
+ if (!relation)
51
+ throw new OrbitError("relation_not_found");
52
+ await w.relations.remove(relationId);
53
+ await record(w, c, "relation_deleted", relation.source_ticket_id, { target_ticket_id: relation.target_ticket_id, type: relation.type });
54
+ });
55
+ c.changed(c.board.id);
56
+ return { ok: true };
57
+ }
58
+ function linkedCard(other) {
59
+ return {
60
+ id: other.id,
61
+ number: other.number,
62
+ title: other.title,
63
+ type: other.type,
64
+ state_id: other.state_id,
65
+ state_name: other.state_name,
66
+ state_role: other.state_role,
67
+ parent_ticket_id: other.parent_ticket_id
68
+ };
69
+ }
70
+ /** Every link touching a card: its relations, then its epic (`child_of`) and its open sub-cards (`parent_of`). */
71
+ export async function ticketRelations(c, ticketId) {
72
+ const found = await c.data.read(async (r) => {
73
+ const ticket = await ticketOn(r, ticketId);
74
+ const lanes = await Lanes.of(r);
75
+ const relations = (await relationRows(r, ticketId, lanes, c.board)).map((row) => ({
76
+ id: row.id,
77
+ type: row.type,
78
+ direction: row.direction,
79
+ source: "relation",
80
+ other_ticket: linkedCard(row.other_ticket)
81
+ }));
82
+ if (ticket.parent_ticket_id) {
83
+ const parent = await r.tickets.get(ticket.parent_ticket_id);
84
+ if (parent && !parent.archived_at)
85
+ relations.push({ id: null, type: "child_of", direction: "outgoing", source: "hierarchy", other_ticket: linkedCard(ticketView(parent, lanes, c.board)) });
86
+ }
87
+ for (const child of await r.tickets.list({ parent: ticketId, archived: false })) {
88
+ relations.push({ id: null, type: "parent_of", direction: "outgoing", source: "hierarchy", other_ticket: linkedCard(laned(child, lanes)) });
89
+ }
90
+ return { ticket_id: ticket.id, relations };
91
+ });
92
+ // Read after this board's own read: links across boards read the other boards.
93
+ return { ...found, cross_board: await crossLinks(c, ticketId) };
94
+ }
95
+ /** What still blocks a card, and whether it can start. */
96
+ export async function ticketBlockers(c, ticketId) {
97
+ return c.data.read(async (r) => {
98
+ const ticket = await ticketOn(r, ticketId);
99
+ const blockers = (await unresolvedBlockers(r, ticketId, await Lanes.of(r))).map((row) => ({
100
+ id: row.id,
101
+ number: row.number,
102
+ title: row.title,
103
+ type: row.type,
104
+ state_id: row.state_id,
105
+ state_name: row.state_name,
106
+ state_role: row.state_role,
107
+ via_epic_id: row.via_epic_id ?? null
108
+ }));
109
+ const waiting = await crossBlockersFor(c, r, ticket.id);
110
+ return { ticket_id: ticket.id, can_start: blockers.length === 0 && waiting.length === 0, blockers, waiting_on: waiting };
111
+ });
112
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Search on one board: open cards whose text, plan, summary, revisions or
3
+ * agent-readable comments match, best first. A query that is a card number
4
+ * (`12`, `#12`) puts that card first.
5
+ */
6
+ import type { BoardCtx } from "./context.ts";
7
+ import type { Id, Label, Ticket } from "./model.ts";
8
+ export interface SearchInput {
9
+ q?: unknown;
10
+ limit?: unknown;
11
+ mode?: unknown;
12
+ include_full?: unknown;
13
+ fields?: unknown;
14
+ max_chars_per_field?: unknown;
15
+ }
16
+ export type SearchMode = "ids" | "summary" | "full";
17
+ /**
18
+ * One hit. Which fields it has depends on the mode: `ids` gives ticket_id,
19
+ * id, number and rank; `summary` (the default) adds title, state_name,
20
+ * state_role, type, priority, parent_ticket_id, a snippet and the labels'
21
+ * names; `full` gives the card's own fields, state_name, state_role,
22
+ * board_slug, rank and the labels as records. With `fields`, only those named.
23
+ */
24
+ export type SearchHit = Partial<Ticket> & {
25
+ ticket_id?: Id;
26
+ rank?: number;
27
+ state_name?: string;
28
+ state_role?: string | null;
29
+ board_slug?: string;
30
+ snippet?: string;
31
+ labels?: string[] | Label[];
32
+ };
33
+ export interface SearchAnswer {
34
+ query: string;
35
+ mode: SearchMode;
36
+ results: SearchHit[];
37
+ }
38
+ export declare function searchTickets(c: BoardCtx, input: SearchInput): Promise<SearchAnswer>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Search on one board: open cards whose text, plan, summary, revisions or
3
+ * agent-readable comments match, best first. A query that is a card number
4
+ * (`12`, `#12`) puts that card first.
5
+ */
6
+ import { cap, positiveInt, asText } from "./text.js";
7
+ import { Lanes, labelsByTicket } from "./views.js";
8
+ function modeOf(input) {
9
+ if (input.include_full === true)
10
+ return "full";
11
+ const mode = asText(input.mode ?? "summary").trim().toLowerCase();
12
+ return mode === "ids" || mode === "full" ? mode : "summary";
13
+ }
14
+ function fieldsOf(fields) {
15
+ if (Array.isArray(fields))
16
+ return new Set(fields.map(String));
17
+ if (typeof fields === "string" && fields.trim())
18
+ return new Set(fields.split(",").map((field) => field.trim()).filter(Boolean));
19
+ return null;
20
+ }
21
+ function pick(row, fields) {
22
+ if (!fields)
23
+ return row;
24
+ const picked = {};
25
+ for (const field of fields)
26
+ if (Object.hasOwn(row, field))
27
+ Object.assign(picked, { [field]: row[field] });
28
+ return picked;
29
+ }
30
+ function numberQuery(q) {
31
+ const match = /^#?(\d+)$/.exec(q.trim());
32
+ if (!match)
33
+ return null;
34
+ const number = Number(match[1]);
35
+ return Number.isSafeInteger(number) ? number : null;
36
+ }
37
+ export async function searchTickets(c, input) {
38
+ const q = asText(input.q ?? "").trim();
39
+ const limit = Math.min(Number(input.limit || 20), 50);
40
+ const mode = modeOf(input);
41
+ const maxChars = positiveInt(input.max_chars_per_field, 0);
42
+ const fields = fieldsOf(input.fields);
43
+ if (!q)
44
+ return { query: q, mode, results: [] };
45
+ const { hits, labels } = await c.data.read(async (r) => {
46
+ const number = numberQuery(q);
47
+ const ranked = new Map();
48
+ if (number !== null) {
49
+ const ticket = await r.tickets.byNumber(number);
50
+ if (ticket && !ticket.archived_at)
51
+ ranked.set(ticket.id, -1);
52
+ }
53
+ for (const hit of await r.search.query(q, { limit }))
54
+ if (!ranked.has(hit.ticket_id))
55
+ ranked.set(hit.ticket_id, hit.rank);
56
+ const ids = [...ranked.keys()];
57
+ if (!ids.length)
58
+ return { hits: [], labels: new Map() };
59
+ const [lanes, tickets] = await Promise.all([Lanes.of(r), r.tickets.list({ ids, archived: false })]);
60
+ const found = tickets.map((ticket) => ({ ...ticket, state_name: lanes.nameOf(ticket), state_role: lanes.roleOf(ticket), board_slug: c.board.slug, rank: ranked.get(ticket.id) ?? 0 }));
61
+ found.sort((a, b) => a.rank - b.rank || b.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(b.id));
62
+ const top = found.slice(0, limit);
63
+ return { hits: top, labels: await labelsByTicket(r, top.map((hit) => hit.id)) };
64
+ });
65
+ const results = hits.map((hit) => {
66
+ const cardLabels = labels.get(hit.id) ?? [];
67
+ if (mode === "ids")
68
+ return pick({ ticket_id: hit.id, id: hit.id, number: hit.number, rank: hit.rank }, fields);
69
+ if (mode === "full") {
70
+ const full = { ...hit, labels: cardLabels };
71
+ if (maxChars)
72
+ for (const key of ["description", "ai_plan", "implementation_summary", "implementation_updates"])
73
+ full[key] = cap(full[key], maxChars);
74
+ return pick(full, fields);
75
+ }
76
+ return pick({
77
+ ticket_id: hit.id,
78
+ id: hit.id,
79
+ number: hit.number,
80
+ title: hit.title,
81
+ state_name: hit.state_name,
82
+ state_role: hit.state_role,
83
+ type: hit.type,
84
+ priority: hit.priority,
85
+ parent_ticket_id: hit.parent_ticket_id,
86
+ rank: hit.rank,
87
+ snippet: cap(hit.description || hit.ai_plan || hit.implementation_summary || "", maxChars || 240),
88
+ labels: cardLabels.map((label) => label.name)
89
+ }, fields);
90
+ });
91
+ return { query: q, mode, results };
92
+ }