@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,113 @@
1
+ /**
2
+ * The small rules every use case shares for what people and agents send:
3
+ * names on one line, slugs, priorities and types read the way the API has
4
+ * always read them, each refusal with its error code.
5
+ */
6
+ import { OrbitError } from "./errors.js";
7
+ /**
8
+ * What a field holds as text: a string as it is, a number or boolean written
9
+ * out, anything else (an object, null, nothing) as "". Input is never
10
+ * stringified as "[object Object]".
11
+ */
12
+ export function asText(value) {
13
+ if (typeof value === "string")
14
+ return value;
15
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
16
+ return String(value);
17
+ return "";
18
+ }
19
+ /** A board slug: lowercase letters, digits and single dashes, at most 80 characters. */
20
+ export function slugify(value) {
21
+ return String(value)
22
+ .toLowerCase()
23
+ .trim()
24
+ .replace(/[^a-z0-9]+/g, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .slice(0, 80);
27
+ }
28
+ /** True for a slug slugify leaves as it is. */
29
+ export function isSlug(value) {
30
+ return typeof value === "string" && value !== "" && slugify(value) === value;
31
+ }
32
+ /** A name as one clean line: line breaks and control characters become spaces. */
33
+ export function cleanName(value) {
34
+ return asText(value ?? "")
35
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ")
36
+ .replace(/\s+/g, " ")
37
+ .trim();
38
+ }
39
+ /** A string with something in it, trimmed; otherwise `missing_<field>`. */
40
+ export function requiredString(value, field) {
41
+ if (typeof value !== "string" || value.trim() === "")
42
+ throw new OrbitError(`missing_${field}`);
43
+ return value.trim();
44
+ }
45
+ /** A lane's name: required, and one clean line. */
46
+ export function laneNameOf(value) {
47
+ const name = cleanName(requiredString(value, "name"));
48
+ if (!name)
49
+ throw new OrbitError("missing_name");
50
+ return name;
51
+ }
52
+ /** 0 to 4, rounded; anything unreadable is med (2). */
53
+ export function normalizePriority(value) {
54
+ const number = Number(value ?? 2);
55
+ if (!Number.isFinite(number))
56
+ return 2;
57
+ return Math.max(0, Math.min(4, Math.round(number)));
58
+ }
59
+ const TICKET_TYPES = ["epic", "feature", "task", "bug"];
60
+ export function normalizeTicketType(value) {
61
+ const type = asText(value || "task").toLowerCase().trim();
62
+ if (!TICKET_TYPES.includes(type))
63
+ throw new OrbitError("invalid_ticket_type");
64
+ return type;
65
+ }
66
+ export function normalizeEntryType(value) {
67
+ const type = asText(value || "").toLowerCase().trim();
68
+ if (type !== "decision" && type !== "lesson")
69
+ throw new OrbitError("invalid_project_entry_type");
70
+ return type;
71
+ }
72
+ const ROLES = ["ai_ready", "in_progress", "review", "done"];
73
+ /** A lane's role in the agent flow, or null for none. */
74
+ export function normalizeStateRole(value) {
75
+ if (value === null || value === undefined || value === "")
76
+ return null;
77
+ const role = asText(value).toLowerCase().trim();
78
+ if (!ROLES.includes(role))
79
+ throw new OrbitError("invalid_state_role");
80
+ return role;
81
+ }
82
+ /** `existing`, then a blank line, then `note`; just `existing` when the note is empty. */
83
+ export function appendNote(existing, note) {
84
+ const text = asText(note ?? "").trim();
85
+ if (!text)
86
+ return existing;
87
+ return [existing.trim(), text].filter(Boolean).join("\n\n");
88
+ }
89
+ /** At most `max` characters; `max` 0 means no cap. */
90
+ export function cap(value, max) {
91
+ const text = asText(value ?? "");
92
+ return max && text.length > max ? text.slice(0, max) : text;
93
+ }
94
+ /** A whole number above zero, else `fallback`. */
95
+ export function positiveInt(value, fallback) {
96
+ const n = Number(value);
97
+ return Number.isInteger(n) && n > 0 ? n : fallback;
98
+ }
99
+ /** A whole number from zero, else `fallback`. */
100
+ export function nonNegativeInt(value, fallback) {
101
+ const n = Number(value);
102
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
103
+ }
104
+ /** An optional string field, trimmed; "" when absent. */
105
+ export function optionalString(value) {
106
+ if (value === undefined || value === null)
107
+ return "";
108
+ return asText(value).trim();
109
+ }
110
+ /** A field that is a string when given, else "" (the API's `body.x || ""`). */
111
+ export function textOr(value, fallback = "") {
112
+ return typeof value === "string" && value !== "" ? value : value === undefined || value === null || value === "" ? fallback : asText(value);
113
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Cards and their comments: making, changing, archiving, restoring and
3
+ * deleting a card, and the discussion on it. Every change records an event,
4
+ * keeps the card's search document current and moves its epic's
5
+ * `updated_at`, so the epic sorts near its sub-cards' work.
6
+ */
7
+ import type { BoardCtx } from "./context.ts";
8
+ import type { Comment, Id, OrbitEvent, Ticket, TicketType } from "./model.ts";
9
+ import type { TicketView } from "./views.ts";
10
+ import type { BoardReader } from "../store/types.ts";
11
+ /** The card's epic, checked: on this board, not itself or its own descendant, and the card isn't an epic. */
12
+ export declare function checkParent(r: BoardReader, value: unknown, type: TicketType, ticketId?: string): Promise<Id | null>;
13
+ export interface NewTicketInput {
14
+ board_id?: unknown;
15
+ title?: unknown;
16
+ description?: unknown;
17
+ type?: unknown;
18
+ parent_ticket_id?: unknown;
19
+ ai_plan?: unknown;
20
+ plan_artifact_path?: unknown;
21
+ implementation_summary?: unknown;
22
+ implementation_artifact_path?: unknown;
23
+ implementation_updates?: unknown;
24
+ nebula_url?: unknown;
25
+ state_id?: unknown;
26
+ priority?: unknown;
27
+ labels?: unknown;
28
+ }
29
+ /** Makes a card, in its lane or the board's default lane. */
30
+ export declare function createTicket(c: BoardCtx, input: NewTicketInput): Promise<TicketView>;
31
+ declare const EDITABLE: readonly ["title", "description", "type", "parent_ticket_id", "ai_plan", "plan_artifact_path", "implementation_summary", "implementation_artifact_path", "implementation_updates", "nebula_url", "state_id", "priority"];
32
+ export type TicketUpdate = Partial<Record<(typeof EDITABLE)[number] | "labels", unknown>>;
33
+ /** Reads a card on this board, or `ticket_not_found`. */
34
+ export declare function ticketOn(r: BoardReader, ticketId: Id): Promise<Ticket>;
35
+ /**
36
+ * Changes a card's fields; `labels` (names) replaces its labels. Moving a card
37
+ * into a done lane deletes the relations that made it a blocker: the relation
38
+ * is the record of what still blocks.
39
+ */
40
+ export declare function updateTicket(c: BoardCtx, ticketId: Id, input: TicketUpdate): Promise<TicketView>;
41
+ /** Puts a card in the archive: off the board, out of search and of agents' work, its data kept. */
42
+ export declare function archiveTicket(c: BoardCtx, ticketId: Id): Promise<TicketView>;
43
+ export declare function restoreTicket(c: BoardCtx, ticketId: Id): Promise<TicketView>;
44
+ /** Deletes an archived card for good, with its comments, links and images; its events stay. */
45
+ export declare function deleteTicket(c: BoardCtx, ticketId: Id): Promise<{
46
+ ok: true;
47
+ deleted_id: Id;
48
+ }>;
49
+ /** Adds a comment to a card: a person's, or an agent's note. */
50
+ export declare function createComment(c: BoardCtx, ticketId: Id, input: {
51
+ body?: unknown;
52
+ author?: unknown;
53
+ kind?: unknown;
54
+ }): Promise<Comment>;
55
+ /** Hides a comment from agents, or shows it again: it leaves (or rejoins) everything an agent can read or search. */
56
+ export declare function setCommentHidden(c: BoardCtx, ticketId: Id, commentId: Id, input: {
57
+ omitted_for_ai?: unknown;
58
+ omittedForAi?: unknown;
59
+ }): Promise<Comment>;
60
+ /** One event of a card's history, its data parsed as `body`. */
61
+ export type HistoryEntry = OrbitEvent & {
62
+ body: unknown;
63
+ };
64
+ /**
65
+ * A card's moves between lanes, oldest first: moves, takes that moved it (into
66
+ * In Progress), asks and finishes, each event's data parsed as `body`.
67
+ */
68
+ export declare function ticketHistory(c: BoardCtx, ticketId: Id): Promise<HistoryEntry[]>;
69
+ export {};
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Cards and their comments: making, changing, archiving, restoring and
3
+ * deleting a card, and the discussion on it. Every change records an event,
4
+ * keeps the card's search document current and moves its epic's
5
+ * `updated_at`, so the epic sorts near its sub-cards' work.
6
+ */
7
+ import { cardLinkFromInput } from "./card-paths.js";
8
+ import { OrbitError } from "./errors.js";
9
+ import { record, reindex, searchDocument, touchTicket } from "./history.js";
10
+ import { normalizeNebulaUrl } from "./nebula-link.js";
11
+ import { normalizePriority, normalizeTicketType, requiredString, textOr, asText } from "./text.js";
12
+ import { Lanes, ticketView } from "./views.js";
13
+ const LABEL_COLOR = "#64748b";
14
+ /** The label with this name, made when the board has none. */
15
+ async function ensureLabel(w, c, labels, name) {
16
+ const existing = labels.find((label) => label.name === name);
17
+ if (existing)
18
+ return existing;
19
+ const label = { id: c.ids.next(), board_id: c.board.id, name, color: LABEL_COLOR, created_at: c.clock.now() };
20
+ await w.labels.insert(label);
21
+ labels.push(label);
22
+ return label;
23
+ }
24
+ /** The card's epic, checked: on this board, not itself or its own descendant, and the card isn't an epic. */
25
+ export async function checkParent(r, value, type, ticketId = "") {
26
+ if (value === undefined || value === null || value === "")
27
+ return null;
28
+ if (type === "epic")
29
+ throw new OrbitError("epic_cannot_have_parent");
30
+ if (value === ticketId)
31
+ throw new OrbitError("ticket_cannot_parent_itself");
32
+ const parent = await r.tickets.get(asText(value));
33
+ if (!parent)
34
+ throw new OrbitError("invalid_parent_ticket");
35
+ // The whole chain up, not one hop: A under B under C under A is a cycle too.
36
+ const seen = new Set();
37
+ let ancestor = parent;
38
+ let guard = 0;
39
+ while (ancestor) {
40
+ if (ticketId && ancestor.id === ticketId)
41
+ throw new OrbitError("ticket_parent_cycle");
42
+ const next = ancestor.parent_ticket_id;
43
+ if (!next || seen.has(next) || guard++ > 1000)
44
+ break;
45
+ seen.add(next);
46
+ ancestor = await r.tickets.get(next);
47
+ }
48
+ return parent.id;
49
+ }
50
+ /** Makes a card, in its lane or the board's default lane. */
51
+ export async function createTicket(c, input) {
52
+ if (input.board_id && input.board_id !== c.board.id)
53
+ throw new OrbitError("board_id_mismatch");
54
+ const title = requiredString(input.title, "title");
55
+ const view = await c.data.write(async (w) => {
56
+ const lanes = await Lanes.of(w);
57
+ const stateId = input.state_id ? asText(input.state_id) : (lanes.list.find((s) => s.is_default) ?? lanes.list[0])?.id;
58
+ if (!stateId)
59
+ throw new OrbitError("board_has_no_states");
60
+ if (!lanes.get(stateId))
61
+ throw new OrbitError("invalid_state");
62
+ const type = normalizeTicketType(input.type || (input.parent_ticket_id ? "feature" : "task"));
63
+ const parentId = await checkParent(w, input.parent_ticket_id, type);
64
+ const nebulaUrl = normalizeNebulaUrl(input.nebula_url);
65
+ const at = c.clock.now();
66
+ const ticket = await w.tickets.create({
67
+ id: c.ids.next(),
68
+ board_id: c.board.id,
69
+ title,
70
+ description: textOr(input.description),
71
+ type,
72
+ parent_ticket_id: parentId,
73
+ ai_plan: textOr(input.ai_plan),
74
+ plan_artifact_path: cardLinkFromInput(c, c.board.id, input.plan_artifact_path),
75
+ implementation_summary: textOr(input.implementation_summary),
76
+ implementation_artifact_path: cardLinkFromInput(c, c.board.id, input.implementation_artifact_path),
77
+ implementation_updates: textOr(input.implementation_updates),
78
+ nebula_url: nebulaUrl,
79
+ state_id: stateId,
80
+ priority: normalizePriority(input.priority),
81
+ created_by: c.actor.name,
82
+ created_at: at,
83
+ updated_at: at,
84
+ archived_at: null
85
+ });
86
+ const names = Array.isArray(input.labels) ? input.labels : [];
87
+ if (names.length) {
88
+ const labels = await w.labels.list();
89
+ for (const name of names) {
90
+ const label = await ensureLabel(w, c, labels, requiredString(name, "label"));
91
+ await w.ticketLabels.add(ticket.id, label.id);
92
+ }
93
+ }
94
+ await record(w, c, "ticket_created", ticket.id, { title, type, parent_ticket_id: parentId });
95
+ await w.search.index(ticket.id, searchDocument(ticket, []));
96
+ await touchTicket(w, parentId, at);
97
+ return ticketView(ticket, lanes, c.board);
98
+ });
99
+ c.changed(c.board.id);
100
+ return view;
101
+ }
102
+ const EDITABLE = [
103
+ "title",
104
+ "description",
105
+ "type",
106
+ "parent_ticket_id",
107
+ "ai_plan",
108
+ "plan_artifact_path",
109
+ "implementation_summary",
110
+ "implementation_artifact_path",
111
+ "implementation_updates",
112
+ "nebula_url",
113
+ "state_id",
114
+ "priority"
115
+ ];
116
+ /** Reads a card on this board, or `ticket_not_found`. */
117
+ export async function ticketOn(r, ticketId) {
118
+ const ticket = await r.tickets.get(ticketId);
119
+ if (!ticket)
120
+ throw new OrbitError("ticket_not_found");
121
+ return ticket;
122
+ }
123
+ /**
124
+ * Changes a card's fields; `labels` (names) replaces its labels. Moving a card
125
+ * into a done lane deletes the relations that made it a blocker: the relation
126
+ * is the record of what still blocks.
127
+ */
128
+ export async function updateTicket(c, ticketId, input) {
129
+ const view = await c.data.write(async (w) => {
130
+ const before = await ticketOn(w, ticketId);
131
+ const lanes = await Lanes.of(w);
132
+ const type = input.type !== undefined ? normalizeTicketType(input.type) : before.type;
133
+ // Each field as it will be stored; the same values drive the event, so history shows what was saved.
134
+ const applied = {};
135
+ for (const field of EDITABLE) {
136
+ const value = input[field];
137
+ if (value === undefined)
138
+ continue;
139
+ if (field === "type")
140
+ applied.type = type;
141
+ else if (field === "title")
142
+ applied.title = requiredString(value, "title");
143
+ else if (field === "priority")
144
+ applied.priority = normalizePriority(value);
145
+ else if (field === "nebula_url")
146
+ applied.nebula_url = normalizeNebulaUrl(value);
147
+ else if (field === "parent_ticket_id")
148
+ applied.parent_ticket_id = await checkParent(w, value, type, before.id);
149
+ else if (field === "plan_artifact_path" || field === "implementation_artifact_path")
150
+ applied[field] = cardLinkFromInput(c, c.board.id, value);
151
+ else
152
+ applied[field] = value;
153
+ }
154
+ // An epic has no parent, said or not.
155
+ if (type === "epic" && applied.parent_ticket_id === undefined)
156
+ applied.parent_ticket_id = null;
157
+ const labelNames = Array.isArray(input.labels) ? input.labels : null;
158
+ const fields = Object.keys(applied);
159
+ if (!fields.length && !labelNames)
160
+ return ticketView(before, lanes, c.board);
161
+ if (applied.state_id && !lanes.get(asText(applied.state_id)))
162
+ throw new OrbitError("invalid_state");
163
+ const at = c.clock.now();
164
+ let after = before;
165
+ if (fields.length)
166
+ after = await w.tickets.update(ticketId, { ...applied, updated_at: at });
167
+ if (labelNames) {
168
+ const labels = await w.labels.list();
169
+ const ids = [];
170
+ for (const raw of labelNames) {
171
+ const name = String(raw ?? "").trim().replace(/\s+/g, " ");
172
+ if (!name)
173
+ continue;
174
+ const label = await ensureLabel(w, c, labels, name);
175
+ if (!ids.includes(label.id))
176
+ ids.push(label.id);
177
+ }
178
+ await w.ticketLabels.set(ticketId, ids);
179
+ if (!fields.length)
180
+ after = await w.tickets.update(ticketId, { updated_at: at });
181
+ }
182
+ if (before.state_id !== after.state_id) {
183
+ if (lanes.roleOf(after) === "done" && lanes.roleOf(before) !== "done") {
184
+ for (const relation of await w.relations.list(ticketId)) {
185
+ const blocker = (relation.source_ticket_id === ticketId && relation.type === "blocks") || (relation.target_ticket_id === ticketId && relation.type === "blocked_by");
186
+ if (blocker)
187
+ await w.relations.remove(relation.id);
188
+ }
189
+ }
190
+ await record(w, c, "state_changed", ticketId, { from: lanes.nameOf(before), to: lanes.nameOf(after), actor_type: c.actor.type, actor_id: c.actor.id });
191
+ }
192
+ else {
193
+ const body = { ...applied };
194
+ if (labelNames)
195
+ body.labels = labelNames;
196
+ if (Object.keys(body).length)
197
+ await record(w, c, "ticket_updated", ticketId, body);
198
+ }
199
+ await reindex(w, ticketId);
200
+ // Both epics changed on a move between them: one lost a sub-card, the other gained one.
201
+ await touchTicket(w, before.parent_ticket_id, at);
202
+ if (after.parent_ticket_id && after.parent_ticket_id !== before.parent_ticket_id)
203
+ await touchTicket(w, after.parent_ticket_id, at);
204
+ return ticketView(after, lanes, c.board);
205
+ });
206
+ c.changed(c.board.id);
207
+ return view;
208
+ }
209
+ async function setArchived(c, ticketId, archive) {
210
+ const { view, changed } = await c.data.write(async (w) => {
211
+ const ticket = await ticketOn(w, ticketId);
212
+ const lanes = await Lanes.of(w);
213
+ if (Boolean(ticket.archived_at) === archive)
214
+ return { view: ticketView(ticket, lanes, c.board), changed: false };
215
+ const at = c.clock.now();
216
+ const after = await w.tickets.update(ticketId, { archived_at: archive ? at : null, updated_at: at });
217
+ await record(w, c, archive ? "ticket_archived" : "ticket_restored", ticketId, { title: ticket.title });
218
+ await touchTicket(w, ticket.parent_ticket_id, at);
219
+ return { view: ticketView(after, lanes, c.board), changed: true };
220
+ });
221
+ if (changed)
222
+ c.changed(c.board.id);
223
+ return view;
224
+ }
225
+ /** Puts a card in the archive: off the board, out of search and of agents' work, its data kept. */
226
+ export function archiveTicket(c, ticketId) {
227
+ return setArchived(c, ticketId, true);
228
+ }
229
+ export function restoreTicket(c, ticketId) {
230
+ return setArchived(c, ticketId, false);
231
+ }
232
+ /** Deletes an archived card for good, with its comments, links and images; its events stay. */
233
+ export async function deleteTicket(c, ticketId) {
234
+ const paths = await c.data.write(async (w) => {
235
+ const ticket = await ticketOn(w, ticketId);
236
+ if (!ticket.archived_at)
237
+ throw new OrbitError("ticket_not_archived");
238
+ const images = await w.attachments.list(ticketId);
239
+ await record(w, c, "ticket_deleted", ticketId, { title: ticket.title, type: ticket.type, board_id: c.board.id });
240
+ await w.tickets.remove(ticketId);
241
+ await touchTicket(w, ticket.parent_ticket_id, c.clock.now());
242
+ return images.map((image) => `attachments/${image.stored_path}`);
243
+ });
244
+ for (const path of paths)
245
+ await c.blobs.remove(c.board.id, path);
246
+ c.changed(c.board.id);
247
+ return { ok: true, deleted_id: ticketId };
248
+ }
249
+ /** Adds a comment to a card: a person's, or an agent's note. */
250
+ export async function createComment(c, ticketId, input) {
251
+ const comment = await c.data.write(async (w) => {
252
+ const ticket = await ticketOn(w, ticketId);
253
+ const body = requiredString(input.body, "body");
254
+ const at = c.clock.now();
255
+ const made = {
256
+ id: c.ids.next(),
257
+ ticket_id: ticketId,
258
+ author: typeof input.author === "string" && input.author ? input.author : c.actor.name,
259
+ kind: typeof input.kind === "string" && input.kind ? input.kind : c.actor.type === "agent" ? "agent_note" : "human_comment",
260
+ body,
261
+ omitted_for_ai: 0,
262
+ created_at: at
263
+ };
264
+ await w.comments.insert(made);
265
+ await w.tickets.update(ticketId, { updated_at: at });
266
+ await record(w, c, "comment_created", ticketId, { kind: made.kind });
267
+ await reindex(w, ticketId);
268
+ await touchTicket(w, ticket.parent_ticket_id, at);
269
+ return made;
270
+ });
271
+ c.changed(c.board.id);
272
+ return comment;
273
+ }
274
+ /** Hides a comment from agents, or shows it again: it leaves (or rejoins) everything an agent can read or search. */
275
+ export async function setCommentHidden(c, ticketId, commentId, input) {
276
+ const comment = await c.data.write(async (w) => {
277
+ const ticket = await ticketOn(w, ticketId);
278
+ const existing = await w.comments.get(commentId);
279
+ if (existing?.ticket_id !== ticketId)
280
+ throw new OrbitError("comment_not_found");
281
+ const hidden = input.omitted_for_ai ?? input.omittedForAi;
282
+ if (typeof hidden !== "boolean")
283
+ throw new OrbitError("omitted_for_ai_required");
284
+ const at = c.clock.now();
285
+ const after = await w.comments.update(commentId, { omitted_for_ai: hidden ? 1 : 0 });
286
+ await w.tickets.update(ticketId, { updated_at: at });
287
+ await record(w, c, "comment_ai_omission_changed", ticketId, { comment_id: commentId, omitted_for_ai: hidden });
288
+ await reindex(w, ticketId);
289
+ await touchTicket(w, ticket.parent_ticket_id, at);
290
+ return after;
291
+ });
292
+ c.changed(c.board.id);
293
+ return comment;
294
+ }
295
+ /**
296
+ * A card's moves between lanes, oldest first: moves, takes that moved it (into
297
+ * In Progress), asks and finishes, each event's data parsed as `body`.
298
+ */
299
+ export async function ticketHistory(c, ticketId) {
300
+ return c.data.read(async (r) => {
301
+ await ticketOn(r, ticketId);
302
+ const events = await r.events.list({ ticketId, types: ["state_changed", "agent_claimed", "checkpoint_requested", "agent_completed"] });
303
+ const entries = events.map((event) => ({ ...event, body: JSON.parse(event.body_json) }));
304
+ // A take that left the card where it was (a takeover in place) moved nothing.
305
+ return entries.filter((entry) => entry.type !== "agent_claimed" || movedLanes(entry.body));
306
+ });
307
+ }
308
+ function movedLanes(body) {
309
+ const { from, to } = (body ?? {});
310
+ return typeof to === "string" && to !== "" && from !== to;
311
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Trello's board export, read as an Orbit export: lists become lanes (roles
3
+ * guessed from their names), cards become cards with their checklists,
4
+ * links and due dates in the description, labels and comments come along.
5
+ * Pure: it only reshapes the data it is given.
6
+ */
7
+ import type { Clock, Ids } from "./context.ts";
8
+ type Raw = Record<string, unknown>;
9
+ export declare function looksLikeTrello(value: unknown): value is Raw;
10
+ /** A Trello export as an Orbit export, for the board `target` names (or one named from Trello's). */
11
+ export declare function trelloToSnapshot(trello: Raw, target: {
12
+ id?: string;
13
+ slug?: string;
14
+ repo_url?: string;
15
+ default_branch?: string;
16
+ }, clock: Clock, ids: Ids): Raw;
17
+ export {};