@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
@@ -1,167 +0,0 @@
1
- import { boardById, compactStatePositions, stateById } from "./queries.js";
2
- import { tx } from "./db.js";
3
- import { requireBoardAccess, requirePermission } from "./auth.js";
4
- import { recordEvent } from "./events.js";
5
- import {
6
- httpError,
7
- id,
8
- normalizeStateRole,
9
- now,
10
- requiredString
11
- } from "./util.js";
12
-
13
- export function createState(boardId, body, ctx) {
14
- const { db, actor } = ctx;
15
- const board = boardById(db, boardId);
16
- if (!board) throw httpError(404, "board_not_found");
17
- requireBoardAccess(actor, board);
18
- requirePermission(actor, "write");
19
-
20
- const role = body.role !== undefined ? normalizeStateRole(body.role) : null;
21
- // role='ai_ready' implies canonical name; reject mismatches so the AI tab
22
- // checkbox always lands a lane named exactly "AI Ready".
23
- const name =
24
- role === "ai_ready"
25
- ? body.name && body.name !== "AI Ready"
26
- ? (() => { throw httpError(409, "ai_ready_lane_must_be_named_ai_ready"); })()
27
- : "AI Ready"
28
- : requiredString(body.name, "name");
29
- const stateId = id();
30
- return tx(db, () => {
31
- if (role) {
32
- db.prepare("UPDATE states SET role = NULL WHERE board_id = ? AND role = ?").run(boardId, role);
33
- }
34
- const maxPosition = db.prepare("SELECT MAX(position) AS value FROM states WHERE board_id = ?").get(boardId).value;
35
- db.prepare(
36
- `INSERT INTO states
37
- (id, board_id, name, position, is_default, role, created_at)
38
- VALUES (?, ?, ?, ?, 0, ?, ?)`
39
- ).run(stateId, boardId, name, Number(maxPosition || 0) + 1, role, now());
40
- recordEvent(db, boardId, "state_created", null, actor.name, {
41
- board_id: boardId,
42
- state_id: stateId,
43
- name,
44
- role
45
- });
46
- return stateById(db, stateId);
47
- });
48
- }
49
-
50
- export function updateState(stateId, body, ctx) {
51
- const { db, actor } = ctx;
52
- const before = stateById(db, stateId);
53
- if (!before) throw httpError(404, "state_not_found");
54
- const board = boardById(db, before.board_id);
55
- requireBoardAccess(actor, board);
56
- requirePermission(actor, "write");
57
-
58
- return tx(db, () => {
59
- const sets = [];
60
- const values = [];
61
- if (body.name !== undefined) {
62
- const nextName = requiredString(body.name, "name");
63
- // The AI Ready lane is the canonical anchor for agent claim-next. Locking
64
- // its name removes a whole class of silent breakage; users who want a
65
- // different label can disable the AI Ready column from Settings → AI.
66
- if (before.role === "ai_ready" && nextName !== "AI Ready") {
67
- throw httpError(409, "cannot_rename_ai_ready_lane");
68
- }
69
- sets.push("name = ?");
70
- values.push(nextName);
71
- }
72
- if (body.is_default !== undefined) {
73
- const nextDefault = body.is_default ? 1 : 0;
74
- if (nextDefault) db.prepare("UPDATE states SET is_default = 0 WHERE board_id = ?").run(before.board_id);
75
- sets.push("is_default = ?");
76
- values.push(nextDefault);
77
- }
78
- if (body.role !== undefined) {
79
- const nextRole = normalizeStateRole(body.role);
80
- // Roles are unique per board: at most one lane plays each role. If another
81
- // lane already holds it, clear that one first so the new assignment wins.
82
- if (nextRole) {
83
- db.prepare("UPDATE states SET role = NULL WHERE board_id = ? AND role = ? AND id != ?")
84
- .run(before.board_id, nextRole, stateId);
85
- }
86
- // Assigning role='ai_ready' implies the lane name must be "AI Ready" (the
87
- // canonical name agents key off). If the caller is also renaming via
88
- // body.name above, that took precedence; otherwise force it here.
89
- if (nextRole === "ai_ready") {
90
- const finalName = body.name !== undefined ? requiredString(body.name, "name") : before.name;
91
- if (finalName !== "AI Ready") {
92
- if (body.name === undefined) {
93
- // Caller didn't ask to rename — silently set the canonical name.
94
- sets.push("name = ?");
95
- values.push("AI Ready");
96
- } else {
97
- throw httpError(409, "ai_ready_lane_must_be_named_ai_ready");
98
- }
99
- }
100
- }
101
- sets.push("role = ?");
102
- values.push(nextRole);
103
- }
104
- if (sets.length === 0) return before;
105
-
106
- values.push(stateId);
107
- db.prepare(`UPDATE states SET ${sets.join(", ")} WHERE id = ?`).run(...values);
108
- recordEvent(db, before.board_id, "state_updated", null, actor.name, {
109
- board_id: before.board_id,
110
- state_id: stateId,
111
- fields: Object.keys(body)
112
- });
113
- return stateById(db, stateId);
114
- });
115
- }
116
-
117
- export function reorderStates(boardId, body, ctx) {
118
- const { db, actor } = ctx;
119
- const board = boardById(db, boardId);
120
- if (!board) throw httpError(404, "board_not_found");
121
- requireBoardAccess(actor, board);
122
- requirePermission(actor, "write");
123
-
124
- const ids = Array.isArray(body.state_ids) ? body.state_ids : [];
125
- const existing = db.prepare("SELECT id FROM states WHERE board_id = ?").all(boardId).map((row) => row.id);
126
- if (ids.length !== existing.length || !existing.every((stateId) => ids.includes(stateId))) {
127
- throw httpError(400, "invalid_state_order");
128
- }
129
-
130
- return tx(db, () => {
131
- ids.forEach((stateId, position) => {
132
- db.prepare("UPDATE states SET position = ? WHERE id = ? AND board_id = ?").run(position, stateId, boardId);
133
- });
134
- recordEvent(db, boardId, "states_reordered", null, actor.name, { board_id: boardId, state_ids: ids });
135
- return db.prepare("SELECT * FROM states WHERE board_id = ? ORDER BY position").all(boardId);
136
- });
137
- }
138
-
139
- export function deleteState(stateId, ctx) {
140
- const { db, actor } = ctx;
141
- const state = stateById(db, stateId);
142
- if (!state) throw httpError(404, "state_not_found");
143
- const board = boardById(db, state.board_id);
144
- requireBoardAccess(actor, board);
145
- requirePermission(actor, "write");
146
-
147
- const ticketCount = db.prepare("SELECT COUNT(*) AS count FROM tickets WHERE state_id = ?").get(stateId).count;
148
- if (ticketCount > 0) throw httpError(409, "state_has_tickets");
149
-
150
- const stateCount = db.prepare("SELECT COUNT(*) AS count FROM states WHERE board_id = ?").get(state.board_id).count;
151
- if (stateCount <= 1) throw httpError(409, "cannot_delete_last_state");
152
-
153
- return tx(db, () => {
154
- db.prepare("DELETE FROM states WHERE id = ?").run(stateId);
155
- compactStatePositions(db, state.board_id);
156
- if (state.is_default) {
157
- const first = db.prepare("SELECT id FROM states WHERE board_id = ? ORDER BY position LIMIT 1").get(state.board_id);
158
- if (first) db.prepare("UPDATE states SET is_default = 1 WHERE id = ?").run(first.id);
159
- }
160
- recordEvent(db, state.board_id, "state_deleted", null, actor.name, {
161
- board_id: state.board_id,
162
- state_id: stateId,
163
- name: state.name
164
- });
165
- return { ok: true, deleted_state_id: stateId };
166
- });
167
- }
@@ -1,109 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { normalizePath } from "./util.js";
4
-
5
- const execFileAsync = promisify(execFile);
6
-
7
- const DEFAULT_TITLE = "Choose repo folder for Orbit board";
8
-
9
- const WINDOWS_FOLDER_PICKER_ARGS = (title) => [
10
- "-NoProfile",
11
- "-STA",
12
- "-Command",
13
- [
14
- "Add-Type -AssemblyName System.Windows.Forms",
15
- "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
16
- `$dialog.Description = ${JSON.stringify(title)}`,
17
- "if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {",
18
- " [Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
19
- " Write-Output $dialog.SelectedPath",
20
- "}"
21
- ].join("; ")
22
- ];
23
-
24
- export function folderPickerCommands(platform = process.platform, title = DEFAULT_TITLE) {
25
- if (platform === "darwin") {
26
- return [
27
- {
28
- command: "osascript",
29
- args: ["-e", `POSIX path of (choose folder with prompt ${JSON.stringify(title)})`]
30
- }
31
- ];
32
- }
33
-
34
- if (platform === "win32") {
35
- return [
36
- {
37
- command: "powershell.exe",
38
- args: WINDOWS_FOLDER_PICKER_ARGS(title)
39
- }
40
- ];
41
- }
42
-
43
- return [
44
- {
45
- command: "zenity",
46
- args: ["--file-selection", "--directory", "--title", title]
47
- },
48
- {
49
- command: "kdialog",
50
- args: ["--getexistingdirectory", ".", title]
51
- },
52
- {
53
- command: "powershell.exe",
54
- args: WINDOWS_FOLDER_PICKER_ARGS(title)
55
- }
56
- ];
57
- }
58
-
59
- function isMissingCommand(error) {
60
- return error?.code === "ENOENT";
61
- }
62
-
63
- function isPickerCancel(error) {
64
- const stderr = String(error?.stderr || "");
65
- return error?.code === 1 && /cancel|user canceled|user cancelled/i.test(stderr);
66
- }
67
-
68
- function windowsPathToWslPath(rawPath) {
69
- const path = String(rawPath || "").trim();
70
- const drivePath = path.match(/^([a-zA-Z]):[\\/](.*)$/);
71
- if (!drivePath) return path;
72
- const drive = drivePath[1].toLowerCase();
73
- const rest = drivePath[2].replace(/\\/g, "/").replace(/^\/+/, "");
74
- return `/mnt/${drive}/${rest}`;
75
- }
76
-
77
- export async function pickFolder(options = {}) {
78
- const platform = options.platform || process.platform;
79
- const title = options.title || DEFAULT_TITLE;
80
- const execFileImpl = options.execFileImpl || execFileAsync;
81
- const commands = folderPickerCommands(platform, title);
82
- let missingCommands = 0;
83
-
84
- for (const spec of commands) {
85
- try {
86
- const result = await execFileImpl(spec.command, spec.args, {
87
- encoding: "utf8",
88
- timeout: 120000,
89
- windowsHide: false
90
- });
91
- const rawPath = String(result.stdout || "").trim();
92
- const selectedPath = normalizePath(platform === "linux" && spec.command === "powershell.exe" ? windowsPathToWslPath(rawPath) : rawPath);
93
- return selectedPath ? { path: selectedPath } : { canceled: true };
94
- } catch (error) {
95
- if (isMissingCommand(error)) {
96
- missingCommands += 1;
97
- continue;
98
- }
99
- if (isPickerCancel(error)) return { canceled: true };
100
- throw error;
101
- }
102
- }
103
-
104
- if (missingCommands === commands.length) {
105
- return { unsupported: true };
106
- }
107
-
108
- return { canceled: true };
109
- }
@@ -1,400 +0,0 @@
1
- import {
2
- addComment,
3
- boardById,
4
- defaultStateId,
5
- ensureLabel,
6
- ticketById
7
- } from "./queries.js";
8
- import { requireBoardAccess, requirePermission } from "./auth.js";
9
- import { tx } from "./db.js";
10
- import { recordEvent } from "./events.js";
11
- import {
12
- httpError,
13
- id,
14
- now,
15
- normalizePriority,
16
- normalizeTicketType,
17
- requiredString
18
- } from "./util.js";
19
-
20
- /**
21
- * Bump a ticket's `updated_at` without recording an event or touching other
22
- * fields. Used to roll child activity up to the parent epic so the column sort
23
- * (ORDER BY updated_at DESC) keeps the epic group near recent activity even
24
- * when only its children changed. Pass `null`/undefined to no-op.
25
- */
26
- export function bumpTicketUpdatedAt(db, ticketId, time) {
27
- if (!ticketId) return;
28
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
29
- }
30
-
31
- export function createTicket(body, ctx) {
32
- const { db, board, actor } = ctx;
33
- // The router resolves `ctx.board` from path/body/query before we get here.
34
- // If the caller also sent `body.board_id`, it must agree with ctx.board.
35
- if (body.board_id && body.board_id !== board.id) {
36
- throw httpError(400, "board_id_mismatch");
37
- }
38
- const boardId = board.id;
39
- const innerBoard = boardById(db, boardId);
40
- if (!innerBoard) throw httpError(404, "board_not_found");
41
- requireBoardAccess(actor, innerBoard);
42
- requirePermission(actor, "write");
43
-
44
- const title = requiredString(body.title, "title");
45
- const stateId = body.state_id || defaultStateId(db, boardId);
46
- const state = db.prepare("SELECT * FROM states WHERE id = ? AND board_id = ?").get(stateId, boardId);
47
- if (!state) throw httpError(400, "invalid_state");
48
-
49
- const ticketType = normalizeTicketType(body.type || (body.parent_ticket_id ? "feature" : "task"));
50
- const parentTicketId = normalizeParentTicketId(db, body.parent_ticket_id, boardId, ticketType);
51
- const ticketId = id();
52
- const time = now();
53
-
54
- return tx(db, () => {
55
- // MAX(number)+1 must be read and consumed inside the transaction: BEGIN
56
- // IMMEDIATE holds the write lock so a concurrent process (e.g. an MCP
57
- // agent creating a ticket) can't pick the same number and trip the
58
- // UNIQUE(board_id, number) constraint.
59
- const nextNumber =
60
- (db.prepare("SELECT MAX(number) AS value FROM tickets WHERE board_id = ?").get(boardId).value || 0) + 1;
61
-
62
- db.prepare(
63
- `INSERT INTO tickets
64
- (id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
65
- plan_artifact_path, implementation_summary, implementation_artifact_path,
66
- implementation_updates, state_id, priority, created_by, created_at, updated_at)
67
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
68
- ).run(
69
- ticketId,
70
- boardId,
71
- nextNumber,
72
- title,
73
- body.description || "",
74
- ticketType,
75
- parentTicketId,
76
- body.ai_plan || "",
77
- body.plan_artifact_path || "",
78
- body.implementation_summary || "",
79
- body.implementation_artifact_path || "",
80
- body.implementation_updates || "",
81
- stateId,
82
- normalizePriority(body.priority),
83
- actor.name,
84
- time,
85
- time
86
- );
87
-
88
- for (const labelName of body.labels || []) {
89
- const label = ensureLabel(db, boardId, labelName);
90
- db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticketId, label.id);
91
- }
92
-
93
- recordEvent(db, boardId, "ticket_created", ticketId, actor.name, {
94
- title,
95
- type: ticketType,
96
- parent_ticket_id: parentTicketId
97
- });
98
- reindexTicket(db, ticketId);
99
- bumpTicketUpdatedAt(db, parentTicketId, time);
100
- return ticketById(db, ticketId);
101
- });
102
- }
103
-
104
- export function updateTicket(ticketId, body, ctx) {
105
- const { db, board, actor } = ctx;
106
- const before = ticketById(db, ticketId);
107
- if (!before) throw httpError(404, "ticket_not_found");
108
- if (before.board_id !== board.id) throw httpError(404, "ticket_not_found");
109
- const innerBoard = boardById(db, before.board_id);
110
- requireBoardAccess(actor, innerBoard);
111
- requirePermission(actor, "write");
112
-
113
- const allowed = [
114
- "title",
115
- "description",
116
- "type",
117
- "parent_ticket_id",
118
- "ai_plan",
119
- "plan_artifact_path",
120
- "implementation_summary",
121
- "implementation_artifact_path",
122
- "implementation_updates",
123
- "state_id",
124
- "priority"
125
- ];
126
-
127
- const nextType = body.type !== undefined ? normalizeTicketType(body.type) : before.type;
128
-
129
- // Resolve every requested field to the value that will actually be written,
130
- // without mutating the caller's request body. The same resolved map drives
131
- // both the UPDATE and the audit event, so history reflects what was stored
132
- // (normalized priority/type, validated parent) rather than raw input.
133
- const applied = {};
134
- for (const field of allowed) {
135
- if (body[field] === undefined) continue;
136
- if (field === "type") applied.type = nextType;
137
- else if (field === "priority") applied.priority = normalizePriority(body.priority);
138
- else if (field === "parent_ticket_id") {
139
- applied.parent_ticket_id = normalizeParentTicketId(db, body.parent_ticket_id, before.board_id, nextType, before.id);
140
- } else applied[field] = body[field];
141
- }
142
- // Promoting a ticket to epic clears any parent, even if the caller didn't
143
- // mention parent_ticket_id (epics cannot have parents).
144
- if (nextType === "epic" && applied.parent_ticket_id === undefined) {
145
- applied.parent_ticket_id = null;
146
- }
147
-
148
- const labelSync = Array.isArray(body.labels);
149
- const fields = Object.keys(applied);
150
-
151
- if (fields.length === 0 && !labelSync) return before;
152
-
153
- if (applied.state_id) {
154
- const state = db.prepare("SELECT * FROM states WHERE id = ? AND board_id = ?").get(applied.state_id, before.board_id);
155
- if (!state) throw httpError(400, "invalid_state");
156
- }
157
-
158
- return tx(db, () => {
159
- const time = now();
160
- if (fields.length > 0) {
161
- const sets = fields.map((field) => `${field} = ?`);
162
- const values = fields.map((field) => applied[field]);
163
- sets.push("updated_at = ?");
164
- values.push(time, ticketId);
165
- db.prepare(`UPDATE tickets SET ${sets.join(", ")} WHERE id = ?`).run(...values);
166
- }
167
-
168
- // Replace ticket labels when `labels` is sent (array of names). Labels are board-scoped.
169
- if (labelSync) {
170
- const boardScopeId = before.board_id;
171
- db.prepare("DELETE FROM ticket_labels WHERE ticket_id = ?").run(ticketId);
172
- for (const labelName of body.labels) {
173
- const clean = String(labelName ?? "")
174
- .trim()
175
- .replace(/\s+/g, " ");
176
- if (!clean) continue;
177
- const label = ensureLabel(db, boardScopeId, clean);
178
- db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticketId, label.id);
179
- }
180
- if (fields.length === 0) {
181
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
182
- }
183
- }
184
-
185
- const after = ticketById(db, ticketId);
186
- if (before.state_id !== after.state_id) {
187
- // Done lane is the auto-resolver for blocker relations. When a ticket
188
- // lands in a role='done' lane, every relation that names this ticket as a
189
- // blocker (`blocks` from this ticket, `blocked_by` toward this ticket)
190
- // becomes meaningless — the relation row is the SSOT, so we delete it.
191
- if (after.state_role === "done" && before.state_role !== "done") {
192
- db.prepare(
193
- `DELETE FROM relations
194
- WHERE (source_ticket_id = ? AND type = 'blocks')
195
- OR (target_ticket_id = ? AND type = 'blocked_by')`
196
- ).run(ticketId, ticketId);
197
- }
198
- recordEvent(db, before.board_id, "state_changed", ticketId, actor.name, {
199
- from: before.state_name,
200
- to: after.state_name,
201
- actor_type: actor.type,
202
- actor_id: actor.id
203
- });
204
- } else {
205
- const eventPayload = { ...applied };
206
- if (labelSync) eventPayload.labels = body.labels;
207
- if (Object.keys(eventPayload).length > 0) {
208
- recordEvent(db, before.board_id, "ticket_updated", ticketId, actor.name, eventPayload);
209
- }
210
- }
211
- reindexTicket(db, ticketId);
212
- // Roll activity up to the parent epic so the column sort keeps the epic
213
- // group near recent child activity. On reparent, bump both old and new
214
- // parent — both views changed (one lost a child, the other gained one).
215
- bumpTicketUpdatedAt(db, before.parent_ticket_id, time);
216
- if (after.parent_ticket_id && after.parent_ticket_id !== before.parent_ticket_id) {
217
- bumpTicketUpdatedAt(db, after.parent_ticket_id, time);
218
- }
219
- return after;
220
- });
221
- }
222
-
223
- export function archiveTicket(ticketId, ctx) {
224
- const { db, board, actor } = ctx;
225
- const ticket = ticketById(db, ticketId);
226
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
227
- requireBoardAccess(actor, boardById(db, ticket.board_id));
228
- requirePermission(actor, "write");
229
-
230
- if (ticket.archived_at) return ticket;
231
-
232
- return tx(db, () => {
233
- const time = now();
234
- db.prepare("UPDATE tickets SET archived_at = ?, updated_at = ? WHERE id = ?").run(time, time, ticketId);
235
- recordEvent(db, ticket.board_id, "ticket_archived", ticketId, actor.name, { title: ticket.title });
236
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
237
- return ticketById(db, ticketId);
238
- });
239
- }
240
-
241
- export function restoreTicket(ticketId, ctx) {
242
- const { db, board, actor } = ctx;
243
- const ticket = ticketById(db, ticketId);
244
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
245
- requireBoardAccess(actor, boardById(db, ticket.board_id));
246
- requirePermission(actor, "write");
247
-
248
- if (!ticket.archived_at) return ticket;
249
-
250
- return tx(db, () => {
251
- const time = now();
252
- db.prepare("UPDATE tickets SET archived_at = NULL, updated_at = ? WHERE id = ?").run(time, ticketId);
253
- recordEvent(db, ticket.board_id, "ticket_restored", ticketId, actor.name, { title: ticket.title });
254
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
255
- return ticketById(db, ticketId);
256
- });
257
- }
258
-
259
- export function deleteTicket(ticketId, ctx) {
260
- const { db, board, actor } = ctx;
261
- const ticket = ticketById(db, ticketId);
262
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
263
- requireBoardAccess(actor, boardById(db, ticket.board_id));
264
- requirePermission(actor, "write");
265
-
266
- if (!ticket.archived_at) throw httpError(409, "ticket_not_archived");
267
-
268
- return tx(db, () => {
269
- recordEvent(db, ticket.board_id, "ticket_deleted", ticketId, actor.name, {
270
- title: ticket.title,
271
- type: ticket.type,
272
- board_id: ticket.board_id
273
- });
274
- db.prepare("DELETE FROM ticket_fts WHERE ticket_id = ?").run(ticketId);
275
- db.prepare("DELETE FROM tickets WHERE id = ?").run(ticketId);
276
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, now());
277
- return { ok: true, deleted_id: ticketId };
278
- });
279
- }
280
-
281
- export function createComment(ticketId, body, ctx) {
282
- const { db, board, actor } = ctx;
283
- const ticket = ticketById(db, ticketId);
284
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
285
- requireBoardAccess(actor, boardById(db, ticket.board_id));
286
- requirePermission(actor, "write");
287
-
288
- const commentBody = requiredString(body.body, "body");
289
- const author = body.author || actor.name;
290
- const kind = body.kind || (actor.type === "agent" ? "agent_note" : "human_comment");
291
-
292
- return tx(db, () => {
293
- const time = now();
294
- const comment = addComment(db, ticketId, author, kind, commentBody);
295
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
296
- recordEvent(db, ticket.board_id, "comment_created", ticketId, actor.name, { kind: comment.kind });
297
- reindexTicket(db, ticketId);
298
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
299
- return comment;
300
- });
301
- }
302
-
303
- export function updateCommentAiOmission(ticketId, commentId, body, ctx) {
304
- const { db, board, actor } = ctx;
305
- const ticket = ticketById(db, ticketId);
306
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
307
- requireBoardAccess(actor, boardById(db, ticket.board_id));
308
- requirePermission(actor, "write");
309
-
310
- const comment = db.prepare("SELECT * FROM comments WHERE id = ? AND ticket_id = ?").get(commentId, ticketId);
311
- if (!comment) throw httpError(404, "comment_not_found");
312
- const omittedForAi = body.omitted_for_ai ?? body.omittedForAi;
313
- if (typeof omittedForAi !== "boolean") throw httpError(400, "omitted_for_ai_required");
314
-
315
- return tx(db, () => {
316
- const time = now();
317
- db.prepare("UPDATE comments SET omitted_for_ai = ? WHERE id = ?").run(omittedForAi ? 1 : 0, commentId);
318
- db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
319
- recordEvent(db, ticket.board_id, "comment_ai_omission_changed", ticketId, actor.name, {
320
- comment_id: commentId,
321
- omitted_for_ai: omittedForAi
322
- });
323
- reindexTicket(db, ticketId);
324
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
325
- return db.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
326
- });
327
- }
328
-
329
- export function getTicketStatusHistory(ticketId, ctx) {
330
- const { db, board, actor } = ctx;
331
- const ticket = ticketById(db, ticketId);
332
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
333
- requireBoardAccess(actor, boardById(db, ticket.board_id));
334
-
335
- return db
336
- .prepare(
337
- `SELECT * FROM events
338
- WHERE ticket_id = ? AND type IN ('state_changed', 'checkpoint_requested', 'agent_completed')
339
- ORDER BY created_at ASC`
340
- )
341
- .all(ticketId)
342
- .map((e) => ({ ...e, body: JSON.parse(e.body_json) }));
343
- }
344
-
345
- export function normalizeParentTicketId(db, value, boardId, ticketType, currentTicketId = "") {
346
- if (value === undefined || value === null || value === "") {
347
- return null;
348
- }
349
- if (ticketType === "epic") throw httpError(400, "epic_cannot_have_parent");
350
- if (value === currentTicketId) throw httpError(400, "ticket_cannot_parent_itself");
351
-
352
- const parent = ticketById(db, value);
353
- if (!parent || parent.board_id !== boardId) throw httpError(400, "invalid_parent_ticket");
354
-
355
- // Walk the full ancestor chain, not just one hop: parenting onto `parent`
356
- // must not make `currentTicketId` its own ancestor (A→B→C→A). The depth cap
357
- // also stops a pre-existing corrupt cycle from looping forever here.
358
- const seen = new Set();
359
- let ancestor = parent;
360
- let guard = 0;
361
- while (ancestor) {
362
- if (currentTicketId && ancestor.id === currentTicketId) {
363
- throw httpError(400, "ticket_parent_cycle");
364
- }
365
- if (!ancestor.parent_ticket_id || seen.has(ancestor.parent_ticket_id) || guard++ > 1000) break;
366
- seen.add(ancestor.parent_ticket_id);
367
- ancestor = ticketById(db, ancestor.parent_ticket_id);
368
- }
369
- return parent.id;
370
- }
371
-
372
- export function reindexTicket(db, ticketId) {
373
- const ticket = db.prepare("SELECT * FROM tickets WHERE id = ?").get(ticketId);
374
- if (!ticket) return;
375
- const comments = db
376
- .prepare("SELECT body FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at ASC")
377
- .all(ticketId)
378
- .map((comment) => comment.body)
379
- .join("\n");
380
- const implementationText = [
381
- `Type: ${ticket.type}`,
382
- ticket.ai_plan,
383
- ticket.implementation_summary,
384
- ticket.implementation_updates
385
- ]
386
- .filter(Boolean)
387
- .join("\n");
388
- db.prepare("DELETE FROM ticket_fts WHERE ticket_id = ?").run(ticketId);
389
- db.prepare("INSERT INTO ticket_fts (ticket_id, title, description, comments) VALUES (?, ?, ?, ?)").run(
390
- ticketId,
391
- ticket.title,
392
- ticket.description,
393
- [implementationText, comments].filter(Boolean).join("\n")
394
- );
395
- }
396
-
397
- export function reindexAllTickets(db) {
398
- const rows = db.prepare("SELECT id FROM tickets").all();
399
- for (const row of rows) reindexTicket(db, row.id);
400
- }