@starlove/orbit 1.4.1 → 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.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.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 -230
  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,10 @@
1
+ /**
2
+ * How the port reads a search query (`SearchReader.query`), shared so every
3
+ * adapter reads it the same way. Words are runs of ASCII letters, digits and
4
+ * `_` holding at least one letter or digit, lowercased; each must start a word
5
+ * of the document. A query with no words is matched as literal text.
6
+ */
7
+ /** The query's words, lowercased, in order. */
8
+ export declare function queryWords(query: string): string[];
9
+ /** The text a query with no words is matched by: the query trimmed, or null when blank. */
10
+ export declare function literalQuery(query: string): string | null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * How the port reads a search query (`SearchReader.query`), shared so every
3
+ * adapter reads it the same way. Words are runs of ASCII letters, digits and
4
+ * `_` holding at least one letter or digit, lowercased; each must start a word
5
+ * of the document. A query with no words is matched as literal text.
6
+ */
7
+ /** The query's words, lowercased, in order. */
8
+ export function queryWords(query) {
9
+ return query
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9_]+/g, " ")
12
+ .split(" ")
13
+ .filter((word) => /[a-z0-9]/.test(word));
14
+ }
15
+ /** The text a query with no words is matched by: the query trimmed, or null when blank. */
16
+ export function literalQuery(query) {
17
+ const text = query.trim();
18
+ return text === "" ? null : text;
19
+ }
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Orbit's storage port: everything the core asks of storage, and nothing about
3
+ * how a database does it. An adapter implements it (SQLite on a computer or
4
+ * server, IndexedDB in the browser, memory for tests, or yours) and proves
5
+ * itself with the conformance suite in ./conformance.ts. docs/ARCHITECTURE.md
6
+ * shows where it sits and how to write one.
7
+ *
8
+ * The shape, top down:
9
+ *
10
+ * OrbitStore one Orbit's data
11
+ * .boards the list of boards (BoardDirectory)
12
+ * .board(id) one board's data (BoardStore)
13
+ * .read(r => ...) a consistent view of what is committed
14
+ * .write(w => ...) one all-or-nothing change, events included
15
+ * .watch(listener) every committed change, from any process or tab
16
+ * BlobStore bytes (images, markdown), apart from records
17
+ *
18
+ * What every adapter promises, beyond each method's own words:
19
+ *
20
+ * - Records go in and come out as plain data, field for field (the types in
21
+ * src/core/model.ts). A record handed out is the caller's copy; changing it
22
+ * changes nothing stored, and neither does changing an object after it was
23
+ * written.
24
+ * - Lists come back in the order each method states, so every adapter answers
25
+ * alike. "Then by when it was written" means the order the records were
26
+ * inserted.
27
+ * - Failures are OrbitErrors (src/core/errors.ts) with the codes named here.
28
+ * A database's own message never reaches an answer: wrap it as
29
+ * `storage_failed`, `storage_busy` or `storage_full`, with the original as
30
+ * its `cause`.
31
+ * - Nothing here makes an id or reads a clock. The core gives every id and
32
+ * time, so the same input gives the same records on any adapter.
33
+ */
34
+ import type { Attachment, Board, BoardChange, BoardEntry, Comment, CrossBoardRelation, Id, Label, OrbitEvent, RegisteredBoard, Relation, ReviewVerdict, State, Ticket, TicketLabel, TicketType, Timestamp } from "../core/model.ts";
35
+ export type { BoardChange } from "../core/model.ts";
36
+ /** One Orbit's data: its boards and its settings. */
37
+ export interface OrbitStore {
38
+ readonly boards: BoardDirectory;
39
+ /** One board's data. Rejects with `board_not_found` when the directory doesn't list it. */
40
+ board(id: Id): Promise<BoardStore>;
41
+ /**
42
+ * The board holding a lane, card, journal entry or relation with this id,
43
+ * or null. A `hint` board is looked at first, so the common case costs one
44
+ * lookup; then board by board in `list()` order.
45
+ */
46
+ locate(kind: "state" | "ticket" | "entry" | "relation", id: Id, hint?: Id): Promise<RegisteredBoard | null>;
47
+ /**
48
+ * Every cross-board relation, on any board, whose target card is on
49
+ * `boardId`, with the board it lives on: board by board in `list()` order,
50
+ * each board's by when each was made, then written. A board that can't be
51
+ * read now (its file is gone) is skipped, never an error: one board's
52
+ * trouble must not stop every other board's cards from opening.
53
+ */
54
+ relationsInto(boardId: Id): Promise<{
55
+ board_id: Id;
56
+ relation: CrossBoardRelation;
57
+ }[]>;
58
+ /** Small values this Orbit keeps for itself (the computer's default board). */
59
+ readonly settings: Settings;
60
+ /** Point-in-time copies of the data, where the adapter can make them; null where it can't. */
61
+ readonly backups: Backups | null;
62
+ /** Lets go of files and connections. The store is unusable afterwards. */
63
+ close(): Promise<void>;
64
+ }
65
+ /** The list of boards. */
66
+ export interface BoardDirectory {
67
+ /** Every board, by name in code-point order (as SQLite's default collation), then by when it was registered. */
68
+ list(): Promise<RegisteredBoard[]>;
69
+ get(id: Id): Promise<RegisteredBoard | null>;
70
+ /** The slug as stored: callers normalise it first. */
71
+ bySlug(slug: string): Promise<RegisteredBoard | null>;
72
+ /** The board used last (`last_active_at`); among equals, the one registered last. Null when there are none. */
73
+ latest(): Promise<RegisteredBoard | null>;
74
+ /**
75
+ * Makes a board: its storage, its own record (`board`) and whatever `seed`
76
+ * writes, in its first write; then lists it, with `last_active_at` set to
77
+ * `board.created_at`. All or nothing: if `seed` throws, no board, no data
78
+ * and no listing remain, and the error is rethrown. Rejects with
79
+ * `board_slug_taken` when another board has the slug, checked as one step
80
+ * with the listing, and with `duplicate_id` when one has the id.
81
+ */
82
+ create(board: Board, seed: (w: BoardWriter) => Promise<void>): Promise<RegisteredBoard>;
83
+ /** Sets `last_active_at` and `updated_at`. Rejects with `board_not_found` for an unknown id. */
84
+ touch(id: Id, at: Timestamp): Promise<void>;
85
+ /**
86
+ * Takes a board off the list and deletes its records, unless `keepData`.
87
+ * Its blobs are the BlobStore's to remove (`removeBoard`). Returns the
88
+ * places it deleted, for people to read (files, for SQLite). Rejects with
89
+ * `board_not_found` for an unknown id.
90
+ */
91
+ remove(id: Id, options?: {
92
+ keepData?: boolean;
93
+ }): Promise<{
94
+ removed: string[];
95
+ }>;
96
+ }
97
+ export interface Settings {
98
+ get(key: string): Promise<string | null>;
99
+ set(key: string, value: string): Promise<void>;
100
+ }
101
+ /** Copies an adapter makes on request: before a destructive change, or after writes. */
102
+ export interface Backups {
103
+ /**
104
+ * Copies one board's data as it is committed now, labelled with `reason`.
105
+ * With `oncePerDay`, nothing is copied when today (UTC) already has one.
106
+ * Returns where the copy went, or null when none was made.
107
+ */
108
+ board(id: Id, reason: string, options?: {
109
+ oncePerDay?: boolean;
110
+ }): Promise<string | null>;
111
+ /** The same for the list of boards and the settings. */
112
+ directory(reason: string, options?: {
113
+ oncePerDay?: boolean;
114
+ }): Promise<string | null>;
115
+ }
116
+ /**
117
+ * One board's data. Reads and writes go through `read()` and `write()`, so
118
+ * every adapter answers the same questions about concurrency the same way.
119
+ */
120
+ export interface BoardStore {
121
+ readonly id: Id;
122
+ /**
123
+ * Runs `body` against what is committed. Everything one read sees is one
124
+ * committed state: a write that commits while `body` runs shows in full or
125
+ * not at all, and an open write's changes never show. Resolves with what
126
+ * `body` returns. A write may wait for reads in progress to finish, so a
127
+ * read's body must not wait for a write of the same board.
128
+ */
129
+ read<T>(body: (r: BoardReader) => Promise<T>): Promise<T>;
130
+ /**
131
+ * Runs `body` as one change to the board. Writes to one board run one at a
132
+ * time across every store over the same data, in this process or another,
133
+ * so nothing a body reads can change under it until it finishes. Its own
134
+ * reads see its own changes. If `body` throws, nothing it did remains (no
135
+ * record, no card number used, no event, no index entry) and the same error
136
+ * is rethrown. When it returns, its changes commit, its events go to every
137
+ * watcher, and `write()` resolves with what it returned.
138
+ *
139
+ * Never call `read()` or `write()` of the same board inside `body`: the
140
+ * inner call waits for the outer one, which is waiting for it. Use `w`.
141
+ * The writer refuses every call (`write_closed`) once `write()` settles.
142
+ */
143
+ write<T>(body: (w: BoardWriter) => Promise<T>): Promise<T>;
144
+ /**
145
+ * Hears every event the board's writes commit, once, in the order they were
146
+ * appended: this store's writes and those of any other store over the same
147
+ * data (another process, another tab). Never an event of a write that
148
+ * failed. Returns the function that stops listening.
149
+ */
150
+ watch(listener: (change: BoardChange) => void): () => void;
151
+ }
152
+ /** What `read()` sees: every kind of record on the board. */
153
+ export interface BoardReader {
154
+ readonly board: BoardRecordReader;
155
+ readonly states: StateReader;
156
+ readonly labels: LabelReader;
157
+ readonly tickets: TicketReader;
158
+ readonly ticketLabels: TicketLabelReader;
159
+ readonly comments: CommentReader;
160
+ readonly relations: RelationReader;
161
+ readonly crossBoardRelations: CrossBoardRelationReader;
162
+ readonly entries: EntryReader;
163
+ readonly verdicts: VerdictReader;
164
+ readonly attachments: AttachmentReader;
165
+ readonly events: EventReader;
166
+ readonly search: SearchReader;
167
+ }
168
+ /**
169
+ * What `write()` changes. It reads too, and sees its own changes. Writes
170
+ * that would break the board's rules of storage are refused with the codes
171
+ * below, and the refusal throws inside `body`, so the write as a whole fails
172
+ * unless `body` catches it.
173
+ *
174
+ * - A record whose id the board already has: `duplicate_id`.
175
+ * - A record naming what the board doesn't have: `invalid_reference`. That is
176
+ * a `board_id` other than this board's, and a card, lane, label or verdict
177
+ * that isn't on the board (a card's lane, a comment's card, a label link's
178
+ * card and label, a relation's two cards, an event's or a journal entry's
179
+ * card when set, a verdict's earlier verdict when set). Two names are not
180
+ * checked: a card's epic (`parent_ticket_id`) and a cross-board relation's
181
+ * target.
182
+ * - Updating or removing a record that isn't there: its kind's not-found
183
+ * code (`state_not_found`, `ticket_not_found`, `comment_not_found`,
184
+ * `relation_not_found`, `board_entry_not_found`, `attachment_not_found`).
185
+ */
186
+ export interface BoardWriter extends BoardReader {
187
+ readonly board: BoardRecordWriter;
188
+ readonly states: StateWriter;
189
+ readonly labels: LabelWriter;
190
+ readonly tickets: TicketWriter;
191
+ readonly ticketLabels: TicketLabelWriter;
192
+ readonly comments: CommentWriter;
193
+ readonly relations: RelationWriter;
194
+ readonly crossBoardRelations: CrossBoardRelationWriter;
195
+ readonly entries: EntryWriter;
196
+ readonly verdicts: VerdictWriter;
197
+ readonly attachments: AttachmentWriter;
198
+ readonly events: EventWriter;
199
+ readonly search: SearchWriter;
200
+ /** Deletes every record on the board, its own record, events and index included, as a replacing import does first. */
201
+ clear(): Promise<void>;
202
+ }
203
+ export interface BoardRecordReader {
204
+ /** Rejects with `board_not_found` only between `clear()` and `set()` inside one write. */
205
+ get(): Promise<Board>;
206
+ }
207
+ export interface BoardRecordWriter extends BoardRecordReader {
208
+ /**
209
+ * Writes the board's own record whole; its id must be this board's
210
+ * (`invalid_reference`). The directory follows its slug, name, repo_url,
211
+ * default_branch and updated_at once the write commits. A slug another
212
+ * board has is refused with `board_slug_taken`.
213
+ */
214
+ set(board: Board): Promise<void>;
215
+ /** Changes the fields given, as `set()` does. */
216
+ update(patch: BoardPatch): Promise<Board>;
217
+ }
218
+ export type BoardPatch = Partial<Omit<Board, "id" | "created_at">>;
219
+ export interface StateReader {
220
+ /** By position, then by when each was written. */
221
+ list(): Promise<State[]>;
222
+ get(id: Id): Promise<State | null>;
223
+ }
224
+ export interface StateWriter extends StateReader {
225
+ /** Refuses a name another lane on the board has with `state_name_taken`. */
226
+ insert(state: State): Promise<void>;
227
+ /** Refuses as insert() does; `state_not_found` for an unknown id. */
228
+ update(id: Id, patch: StatePatch): Promise<State>;
229
+ /** Refuses with `state_has_tickets` while any card, archived or not, is in the lane. */
230
+ remove(id: Id): Promise<void>;
231
+ }
232
+ export type StatePatch = Partial<Pick<State, "name" | "position" | "is_default" | "role">>;
233
+ export interface LabelReader {
234
+ /** By name in code-point order. */
235
+ list(): Promise<Label[]>;
236
+ }
237
+ export interface LabelWriter extends LabelReader {
238
+ /** Refuses a name another label on the board has with `label_name_taken`. */
239
+ insert(label: Label): Promise<void>;
240
+ }
241
+ export interface TicketFilter {
242
+ /** true: archived cards only; false: open cards only; absent: both. */
243
+ archived?: boolean;
244
+ /** Only the sub-cards of this epic. */
245
+ parent?: Id;
246
+ /** Only the cards in this lane. */
247
+ state?: Id;
248
+ /** Only these cards (unknown ids are skipped). */
249
+ ids?: readonly Id[];
250
+ }
251
+ export interface TicketReader {
252
+ get(id: Id): Promise<Ticket | null>;
253
+ byNumber(number: number): Promise<Ticket | null>;
254
+ /**
255
+ * The card with this exact title, ignoring the case of ASCII letters (as
256
+ * SQLite's lower() does): an open card before an archived one, then the
257
+ * highest number.
258
+ */
259
+ byTitle(title: string): Promise<Ticket | null>;
260
+ /** The cards matching every filter given, by number. */
261
+ list(filter?: TicketFilter): Promise<Ticket[]>;
262
+ /** The highest number on the board, archived cards included; 0 when it has none. */
263
+ lastNumber(): Promise<number>;
264
+ }
265
+ /** A new card: everything but its number, which storage assigns. */
266
+ export type NewTicket = Omit<Ticket, "number">;
267
+ export interface TicketWriter extends TicketReader {
268
+ /**
269
+ * Makes a card numbered one more than the highest number on the board now
270
+ * (archived cards count; the first card is 1), and returns it. Because
271
+ * writes run one at a time, two callers never get the same number, and a
272
+ * write that fails uses none.
273
+ */
274
+ create(ticket: NewTicket): Promise<Ticket>;
275
+ /** Writes a card with its number as given (imports). Refuses a number in use with `ticket_number_taken`. */
276
+ insert(ticket: Ticket): Promise<void>;
277
+ /** Changes the fields given. Refuses an unknown lane with `invalid_reference`. */
278
+ update(id: Id, patch: TicketPatch): Promise<Ticket>;
279
+ /**
280
+ * Deletes a card and what belongs to it: its comments, label links,
281
+ * relations either way, cross-board relations from it, review verdicts,
282
+ * images (records; the bytes are the BlobStore's) and index entry. Events
283
+ * and journal entries about it stay, their `ticket_id` set to null. Its
284
+ * sub-cards keep their `parent_ticket_id`.
285
+ */
286
+ remove(id: Id): Promise<void>;
287
+ }
288
+ export type TicketPatch = Partial<Omit<Ticket, "id" | "board_id" | "number" | "created_by" | "created_at">>;
289
+ export interface TicketLabelReader {
290
+ /** The links of these cards (every card's when absent), in the order they were written. */
291
+ list(ticketIds?: readonly Id[]): Promise<TicketLabel[]>;
292
+ }
293
+ export interface TicketLabelWriter extends TicketLabelReader {
294
+ /** Replaces the card's labels with these. */
295
+ set(ticketId: Id, labelIds: readonly Id[]): Promise<void>;
296
+ /** Adds one; adding a link that exists changes nothing. */
297
+ add(ticketId: Id, labelId: Id): Promise<void>;
298
+ }
299
+ export interface CommentReader {
300
+ get(id: Id): Promise<Comment | null>;
301
+ /** A card's comments (every card's when absent), oldest first, then by when each was written. */
302
+ list(ticketId?: Id): Promise<Comment[]>;
303
+ /**
304
+ * How many comments each card has; cards with none are left out. With
305
+ * `forAgents`, comments hidden from agents don't count; with `exceptBody`,
306
+ * neither do comments whose body is exactly that text.
307
+ */
308
+ countByTicket(options?: {
309
+ forAgents?: boolean;
310
+ exceptBody?: string;
311
+ }): Promise<{
312
+ ticket_id: Id;
313
+ count: number;
314
+ }[]>;
315
+ }
316
+ export interface CommentWriter extends CommentReader {
317
+ insert(comment: Comment): Promise<void>;
318
+ update(id: Id, patch: Pick<Comment, "omitted_for_ai">): Promise<Comment>;
319
+ }
320
+ export interface RelationReader {
321
+ get(id: Id): Promise<Relation | null>;
322
+ /** The relations with this card on either side (every relation when absent), by when each was made, then written. */
323
+ list(ticketId?: Id): Promise<Relation[]>;
324
+ }
325
+ export interface RelationWriter extends RelationReader {
326
+ /** Refuses a second relation of one type between the same two cards, in the same direction, with `relation_exists`. */
327
+ insert(relation: Relation): Promise<void>;
328
+ remove(id: Id): Promise<void>;
329
+ }
330
+ export interface CrossBoardRelationReader {
331
+ /** The cross-board relations from this card (all of the board's when absent), by when each was made, then written. */
332
+ list(ticketId?: Id): Promise<CrossBoardRelation[]>;
333
+ }
334
+ export interface CrossBoardRelationWriter extends CrossBoardRelationReader {
335
+ /** The source card must be on this board; the target is not checked here. Refuses a repeat with `relation_exists`. */
336
+ insert(relation: CrossBoardRelation): Promise<void>;
337
+ remove(id: Id): Promise<void>;
338
+ }
339
+ export interface EntryReader {
340
+ get(id: Id): Promise<BoardEntry | null>;
341
+ /** Oldest first, then by when each was written. */
342
+ list(): Promise<BoardEntry[]>;
343
+ }
344
+ export interface EntryWriter extends EntryReader {
345
+ insert(entry: BoardEntry): Promise<void>;
346
+ update(id: Id, patch: Partial<Pick<BoardEntry, "struck_at" | "updated_at">>): Promise<BoardEntry>;
347
+ /** Refuses an id that isn't there with `board_entry_not_found`. */
348
+ remove(id: Id): Promise<void>;
349
+ }
350
+ export interface VerdictReader {
351
+ get(id: Id): Promise<ReviewVerdict | null>;
352
+ /** A card's verdicts (every card's when absent), oldest first, then by when each was written. */
353
+ list(ticketId?: Id): Promise<ReviewVerdict[]>;
354
+ }
355
+ export interface VerdictWriter extends VerdictReader {
356
+ insert(verdict: ReviewVerdict): Promise<void>;
357
+ }
358
+ export interface AttachmentReader {
359
+ get(id: Id): Promise<Attachment | null>;
360
+ /** A card's images (every card's when absent), oldest first, then by when each was written. */
361
+ list(ticketId?: Id): Promise<Attachment[]>;
362
+ }
363
+ export interface AttachmentWriter extends AttachmentReader {
364
+ insert(attachment: Attachment): Promise<void>;
365
+ remove(id: Id): Promise<void>;
366
+ }
367
+ export interface EventFilter {
368
+ ticketId?: Id;
369
+ /** Only events of these types. */
370
+ types?: readonly string[];
371
+ /** Only events made at or after this time. */
372
+ since?: Timestamp;
373
+ }
374
+ export interface EventReader {
375
+ /** The events matching every filter given, oldest first, then in the order they were appended. */
376
+ list(filter?: EventFilter): Promise<OrbitEvent[]>;
377
+ /** For each of these cards, its newest event of these types (oldest-first order's last); cards with none are left out. */
378
+ latest(ticketIds: readonly Id[], types: readonly string[]): Promise<OrbitEvent[]>;
379
+ /** Up to `limit` events appended after the event with this id, in the order they were appended; none for an id the board doesn't have. */
380
+ after(id: Id, limit: number): Promise<OrbitEvent[]>;
381
+ /** The event appended last, or null. */
382
+ last(): Promise<OrbitEvent | null>;
383
+ }
384
+ export interface EventWriter extends EventReader {
385
+ /** Records an event; watchers hear it once the write commits. */
386
+ append(event: OrbitEvent): Promise<void>;
387
+ }
388
+ /**
389
+ * What the index holds for one card. The core builds it, so whatever must
390
+ * never be found (comments hidden from agents) is simply never given.
391
+ */
392
+ export interface SearchDocument {
393
+ title: string;
394
+ description: string;
395
+ /** Matched by word queries only. */
396
+ type: TicketType;
397
+ /** The plan, the summary and the revisions, one per line. */
398
+ details: string;
399
+ /** The comments agents may read, one per line. */
400
+ comments: string;
401
+ }
402
+ export interface SearchHit {
403
+ ticket_id: Id;
404
+ /** The adapter's own score; lower is better. */
405
+ rank: number;
406
+ }
407
+ export interface SearchReader {
408
+ /**
409
+ * The open (unarchived) cards whose documents match, best first, at most
410
+ * `limit`. The query's words are its runs of ASCII letters, digits and `_`
411
+ * that hold a letter or digit, lowercased (`src/store/search-words.ts`). A
412
+ * document's words are its runs of letters, digits and `_`, so `snake_case`
413
+ * is one word. A card matches when, for every query word, its document has
414
+ * a word starting with it, in any case (an adapter may also fold accents).
415
+ * A query with no such word matches documents holding the query's text as
416
+ * it is (trimmed), ignoring the case of ASCII letters; `%`, `_` and `\` in
417
+ * it are only themselves. A blank query matches nothing.
418
+ */
419
+ query(text: string, options: {
420
+ limit: number;
421
+ }): Promise<SearchHit[]>;
422
+ }
423
+ export interface SearchWriter extends SearchReader {
424
+ /** Replaces the card's document. */
425
+ index(ticketId: Id, document: SearchDocument): Promise<void>;
426
+ remove(ticketId: Id): Promise<void>;
427
+ }
428
+ /**
429
+ * Bytes Orbit keeps beside a board's records: images under `attachments/`,
430
+ * markdown artifacts under `artifacts/`. Paths are POSIX-style and relative
431
+ * to the board (`attachments/tickets/<card id>/<image id>.png`); one that is
432
+ * absolute or climbs out with `..` is refused with `invalid_blob_path`.
433
+ * Blobs aren't part of a board's writes: the core writes the bytes before
434
+ * the record that names them and removes them after the record is gone, so a
435
+ * failure leaves an unused blob, never a record without its bytes.
436
+ */
437
+ export interface BlobStore {
438
+ /** Stores the bytes, replacing what is there; with `exclusive`, refuses a path in use with `blob_exists`. */
439
+ put(boardId: Id, path: string, bytes: Uint8Array, options?: {
440
+ exclusive?: boolean;
441
+ }): Promise<void>;
442
+ /** The bytes, or null when nothing is stored at the path. */
443
+ get(boardId: Id, path: string): Promise<Uint8Array | null>;
444
+ /** The size in bytes, or null when nothing is stored at the path. */
445
+ size(boardId: Id, path: string): Promise<number | null>;
446
+ /** Removing what isn't there changes nothing. */
447
+ remove(boardId: Id, path: string): Promise<void>;
448
+ /** Removes every blob of the board. */
449
+ removeBoard(boardId: Id): Promise<void>;
450
+ /** Where the bytes are on this machine, for answers that show it; absent where blobs aren't files. */
451
+ location?(boardId: Id, path: string): string;
452
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Orbit's storage port: everything the core asks of storage, and nothing about
3
+ * how a database does it. An adapter implements it (SQLite on a computer or
4
+ * server, IndexedDB in the browser, memory for tests, or yours) and proves
5
+ * itself with the conformance suite in ./conformance.ts. docs/ARCHITECTURE.md
6
+ * shows where it sits and how to write one.
7
+ *
8
+ * The shape, top down:
9
+ *
10
+ * OrbitStore one Orbit's data
11
+ * .boards the list of boards (BoardDirectory)
12
+ * .board(id) one board's data (BoardStore)
13
+ * .read(r => ...) a consistent view of what is committed
14
+ * .write(w => ...) one all-or-nothing change, events included
15
+ * .watch(listener) every committed change, from any process or tab
16
+ * BlobStore bytes (images, markdown), apart from records
17
+ *
18
+ * What every adapter promises, beyond each method's own words:
19
+ *
20
+ * - Records go in and come out as plain data, field for field (the types in
21
+ * src/core/model.ts). A record handed out is the caller's copy; changing it
22
+ * changes nothing stored, and neither does changing an object after it was
23
+ * written.
24
+ * - Lists come back in the order each method states, so every adapter answers
25
+ * alike. "Then by when it was written" means the order the records were
26
+ * inserted.
27
+ * - Failures are OrbitErrors (src/core/errors.ts) with the codes named here.
28
+ * A database's own message never reaches an answer: wrap it as
29
+ * `storage_failed`, `storage_busy` or `storage_full`, with the original as
30
+ * its `cause`.
31
+ * - Nothing here makes an id or reads a clock. The core gives every id and
32
+ * time, so the same input gives the same records on any adapter.
33
+ */
34
+ export {};
package/llms.txt ADDED
@@ -0,0 +1,63 @@
1
+ # Starscape Orbit
2
+
3
+ > Orbit is a local-first kanban board that AI agents use for project management: tasks, tickets, plans, decisions, and lessons. The human watches the work move on a calm, live board. It's free and open source (AGPL-3.0), published on npm as `@starlove/orbit`, and needs Node.js 22.16+ (or 24).
4
+
5
+ If your user said something like "install and use Starscape Orbit", this page is for you, the agent. Follow the setup below, then use Orbit for this user's project tracking from now on.
6
+
7
+ ## Set it up
8
+
9
+ 1. **Check Node.js.** `node -v` must print 22.16 or newer (24 is best; 23 won't do). If it doesn't, ask the user before installing or upgrading Node.
10
+ 2. **Install Orbit.** Run `npm install -g @starlove/orbit` (skip if `orbit --version` already prints 2 or later). Orbit keeps every board in its data folder, `~/.starscape/orbit`. If the user had Orbit 1.x (boards in `~/.orbit`), Orbit says their old boards are waiting: run `orbit migrate`, which copies them over and only reads the old folder.
11
+ 3. **Connect yourself to Orbit's MCP server.** It's a stdio server started with `orbit mcp`. Use the entry for your harness:
12
+ - Claude Code: `claude mcp add --scope user orbit -- orbit mcp`
13
+ - Codex CLI: `codex mcp add orbit -- orbit mcp`
14
+ - Hermes Agent, in `~/.hermes/config.yaml`:
15
+ ```yaml
16
+ mcp_servers:
17
+ orbit:
18
+ command: orbit
19
+ args: [mcp]
20
+ ```
21
+ - Cursor (`~/.cursor/mcp.json`), Claude Desktop (`claude_desktop_config.json`), Gemini CLI (`~/.gemini/settings.json`), and most other clients:
22
+ ```json
23
+ { "mcpServers": { "orbit": { "command": "orbit", "args": ["mcp"] } } }
24
+ ```
25
+ - VS Code's GitHub Copilot and Copilot CLI share `~/.copilot/mcp-config.json`: `{ "mcpServers": { "orbit": { "type": "stdio", "command": "orbit", "args": ["mcp"], "tools": ["*"] } } }`
26
+
27
+ Add `-e ORBIT_AGENT_NAME=<your name>` (or an `env` entry) so the board shows who did what.
28
+
29
+ Desktop apps often can't see the shell's PATH. If `orbit` isn't found, use absolute paths: set the command to the output of `command -v node`, and set the args to `["<output of npm root -g>/@starlove/orbit/lib/cli/orbit.js", "mcp"]`. Most clients load MCP servers at startup, so tell the user if they need to restart the app or open a new session.
30
+ 4. **Start the board app** so the user can see it. If `curl -s http://127.0.0.1:13701/api/health` fails, run `orbit run -d`, which starts Orbit in the background. Give the user the URL, normally http://localhost:13701.
31
+ 5. **Create a board for the project.** Send `boards` (orbit_read) to see what exists. If no board fits, send `boards new <project name>` (orbit_write). From a shell it's `orbit boards` and `orbit boards new <name>`.
32
+ 6. **Remember it.** Save a durable note wherever you keep standing instructions (your memory, `AGENTS.md`, `CLAUDE.md`, rules files): *"Use Starscape Orbit for project management: tasks, tickets, kanban, plans, decisions. Board: `<slug>`. Start with `orbit view <slug>` (or orbit_read `view <slug>`); `help` lists the commands."* So every agent in a repo lands on its board, run `orbit boards link <slug>` in the repo and commit the `orbit.json` it writes (one line: `{ "board": "<slug>" }`); commands that name no board then use it. Otherwise, with several boards, name the board in each command (`view <slug>`, `<slug>#12`, `--board <slug>`), or make it the default for every folder on this computer with `orbit boards use <slug>`. Ask before editing files other people share.
33
+ 7. **Tell the user it's ready.** Give them the board URL and the board name, and explain how to hand you work: "Move a card to AI Ready, or say 'work on ticket 12'."
34
+
35
+ ## Run it on a server
36
+
37
+ Orbit has no login yet, so keep it on a private network.
38
+
39
+ - On the server: `npm install -g @starlove/orbit && DATA_DIR=/var/lib/orbit orbit run -d`. For restarts after a reboot, run it under systemd, or build the `Dockerfile` that ships with Orbit and run the container with a restart policy.
40
+ - Reach it privately with `tailscale serve --bg 13701` (only your tailnet can reach it) or an SSH tunnel: `ssh -N -L 13701:127.0.0.1:13701 user@server`.
41
+ - To connect an agent on another machine, use remote mode: the same MCP entry, plus the environment variables `ORBIT_MODE=remote`, `ORBIT_API_URL=<private URL>`, and optionally `ORBIT_BOARD=<slug>`. For example:
42
+ `claude mcp add orbit --scope user -e ORBIT_MODE=remote -e ORBIT_API_URL=https://orbit.your-tailnet.ts.net -e ORBIT_AGENT_NAME=claude -- orbit mcp`
43
+ - The `orbit` CLI takes the same variables, and every Orbit serves its own `/llms.txt` with the commands for that version.
44
+ - Never put Orbit on a public address.
45
+
46
+ ## Day to day
47
+
48
+ Orbit speaks one small command language. Over MCP you get two tools, orbit_read and orbit_write, each taking one command line, with long text in `text`. In a shell the same commands are `orbit view`, `orbit take`, `orbit finish 12 -m "..."`. Cards are #n (in a shell write `orbit card 12`, because a word starting with `#` is a comment), lanes are named, priorities are words. AI Ready holds cards an agent may take: each says its goal and when it's done. The core loop:
49
+
50
+ 1. `view` shows the board, one line per card.
51
+ 2. `take` claims the next card in AI Ready, or `take #12` the card the user names. It returns the card with the board's rules and next steps.
52
+ 3. If the goal or "done" is unclear, `ask #12` with what's missing, then stop.
53
+ 4. `finish #12` with what changed and how you checked it. The card moves to Review for the human.
54
+
55
+ When the user mentions new work, `new <title>` makes a card instead of a private list. `help` lists every command; the guide linked below has the whole workflow. Never edit Orbit's SQLite files directly.
56
+
57
+ ## Links
58
+
59
+ - [Agent guide](https://orbit.starscape.app/guide.md): the full workflow, every command, and the HTTP API
60
+ - [README](https://orbit.starscape.app/README.md): install, storage, backups, and self-hosting
61
+ - [npm package](https://www.npmjs.com/package/@starlove/orbit)
62
+ - [Source](https://github.com/brucestarlove/ss-orbit)
63
+ - [Live demo](https://orbit.starscape.app/app/): runs in the browser only, so agents can't connect to it