@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
package/src/core/agent.js DELETED
@@ -1,387 +0,0 @@
1
- import {
2
- addComment,
3
- boardById,
4
- childTicketsFor,
5
- compactTicket,
6
- labelsForTicket,
7
- relationRows,
8
- stateByName,
9
- stateByRole,
10
- stateById,
11
- ticketById,
12
- ticketByNumber,
13
- ticketByTitle,
14
- uniqueTickets,
15
- unresolvedBlockers
16
- } from "./queries.js";
17
- import { canAccessBoard, requireBoardAccess } from "./auth.js";
18
- import { tx } from "./db.js";
19
- import { recordEvent } from "./events.js";
20
- import {
21
- appendFieldNote,
22
- httpError,
23
- now,
24
- normalizeTicketType,
25
- requiredString
26
- } from "./util.js";
27
- import { boardManual } from "./boards.js";
28
- import { bumpTicketUpdatedAt, reindexTicket } from "./tickets.js";
29
-
30
- /**
31
- * Claim the next AI-ready ticket on the resolved board. The router/MCP layer
32
- * is responsible for picking which board this runs against (via path slug,
33
- * body field, or session board); this function never scans other boards or
34
- * mutates global state. If you want to claim from another board, the caller
35
- * resolves a different ctx and calls again.
36
- */
37
- export function claimNext(body, ctx) {
38
- const { db, board, actor } = ctx;
39
- const filters = {
40
- type: body.type ? normalizeTicketType(body.type) : "",
41
- includeEpics: Boolean(body.include_epics)
42
- };
43
-
44
- const inner = boardById(db, board.id);
45
- if (!inner || !canAccessBoard(actor, inner)) {
46
- throw httpError(403, "forbidden");
47
- }
48
-
49
- const candidates = db
50
- .prepare(
51
- `SELECT t.*, b.slug AS board_slug
52
- FROM tickets t
53
- JOIN states s ON s.id = t.state_id
54
- JOIN boards b ON b.id = t.board_id
55
- WHERE (s.role = 'ai_ready' OR (s.role IS NULL AND s.name = 'AI Ready'))
56
- AND t.archived_at IS NULL
57
- ORDER BY t.priority DESC, t.created_at ASC`
58
- )
59
- .all();
60
-
61
- const filtered = candidates.filter((ticket) =>
62
- !filters.type ? filters.includeEpics || ticket.type !== "epic" : ticket.type === filters.type
63
- );
64
-
65
- for (const candidate of filtered) {
66
- const blockedBy = unresolvedBlockers(db, candidate.id);
67
- if (blockedBy.length > 0) continue;
68
-
69
- const inProgress =
70
- stateByRole(db, candidate.board_id, "in_progress") ||
71
- stateByName(db, candidate.board_id, "In Progress");
72
- if (!inProgress) throw httpError(400, "missing_in_progress_state");
73
-
74
- tx(db, () => {
75
- const time = now();
76
- db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(
77
- inProgress.id,
78
- time,
79
- candidate.id
80
- );
81
- addComment(db, candidate.id, actor.name, "agent_note", "Agent claimed this ticket and is starting work.");
82
- recordEvent(db, candidate.board_id, "agent_claimed", candidate.id, actor.name, { agent_id: actor.id });
83
- reindexTicket(db, candidate.id);
84
- bumpTicketUpdatedAt(db, candidate.parent_ticket_id, time);
85
- });
86
-
87
- return {
88
- claimed: true,
89
- ticket_id: candidate.id,
90
- context: readTicket(candidate.id, ctx)
91
- };
92
- }
93
-
94
- return { claimed: false, reason: "no_schedulable_ticket" };
95
- }
96
-
97
- function resolveTicketLookup(db, boardId, { ticket_id, number, title }) {
98
- if (ticket_id) return ticketById(db, ticket_id);
99
- if (number !== undefined && number !== null && number !== "") {
100
- const n = Number(number);
101
- if (!Number.isInteger(n)) throw httpError(400, "invalid_number");
102
- return ticketByNumber(db, boardId, n);
103
- }
104
- if (title) return ticketByTitle(db, boardId, String(title));
105
- throw httpError(400, "ticket_id_or_number_or_title_required");
106
- }
107
-
108
- export function readTicket(lookup, ctx) {
109
- const { db, board, actor } = ctx;
110
- const args = typeof lookup === "string" ? { ticket_id: lookup } : lookup || {};
111
- const ticket = resolveTicketLookup(db, board.id, args);
112
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
113
- const innerBoard = boardById(db, ticket.board_id);
114
- requireBoardAccess(actor, innerBoard);
115
-
116
- const labels = labelsForTicket(db, ticket.id);
117
-
118
- return {
119
- ticket: {
120
- id: ticket.id,
121
- number: ticket.number,
122
- title: ticket.title,
123
- description: ticket.description,
124
- state_name: ticket.state_name,
125
- type: ticket.type,
126
- priority: ticket.priority,
127
- labels
128
- },
129
- board_manual: boardManual(innerBoard.id, ctx)
130
- };
131
- }
132
-
133
- export function readComments(lookup, ctx) {
134
- const { db, board, actor } = ctx;
135
- const args = typeof lookup === "string" ? { ticket_id: lookup } : lookup || {};
136
- const ticket = resolveTicketLookup(db, board.id, args);
137
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
138
- requireBoardAccess(actor, boardById(db, ticket.board_id));
139
- const comments = db
140
- .prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
141
- .all(ticket.id);
142
- return { ticket_id: ticket.id, comments };
143
- }
144
-
145
- export function checkpointTicket(body, ctx) {
146
- const { db, board, actor } = ctx;
147
- const tid = requiredString(body.ticket_id, "ticket_id");
148
- const ticket = ticketById(db, tid);
149
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
150
- requireBoardAccess(actor, boardById(db, ticket.board_id));
151
-
152
- // Same review lane as `complete`; distinguish mid-flight pause via checkpoint comment + event kind.
153
- const review =
154
- stateByRole(db, ticket.board_id, "review") ||
155
- stateByName(db, ticket.board_id, "Review");
156
- if (!review) throw httpError(400, "missing_review_state");
157
- const message = requiredString(body.message, "message");
158
- tx(db, () => {
159
- const time = now();
160
- db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(review.id, time, ticket.id);
161
- addComment(db, ticket.id, actor.name, "checkpoint", message);
162
- recordEvent(db, ticket.board_id, "checkpoint_requested", ticket.id, actor.name, {
163
- from: ticket.state_name,
164
- state: review.name,
165
- to: review.name,
166
- actor_type: actor.type,
167
- actor_id: actor.id
168
- });
169
- reindexTicket(db, ticket.id);
170
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
171
- });
172
- return getContextPack(ticket.id, ctx, 1);
173
- }
174
-
175
- export function completeTicket(body, ctx) {
176
- const { db, board, actor } = ctx;
177
- const tid = requiredString(body.ticket_id, "ticket_id");
178
- const ticket = ticketById(db, tid);
179
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
180
- requireBoardAccess(actor, boardById(db, ticket.board_id));
181
-
182
- // If the caller specified a next_state, honor that name verbatim. Otherwise
183
- // resolve the review lane by role so renaming the lane does not break this.
184
- const nextState = body.next_state
185
- ? stateByName(db, ticket.board_id, body.next_state)
186
- : stateByRole(db, ticket.board_id, "review") || stateByName(db, ticket.board_id, "Review");
187
- if (!nextState) throw httpError(400, "missing_next_state");
188
-
189
- const summary = requiredString(body.summary, "summary");
190
- const lines = [summary];
191
- if (body.pr_url) lines.push(`PR: ${body.pr_url}`);
192
- const updates = body.updates ? appendFieldNote(ticket.implementation_updates, body.updates, actor.name) : ticket.implementation_updates;
193
- const implementationArtifactPath = body.implementation_artifact_path || body.summary_artifact_path || ticket.implementation_artifact_path || "";
194
- tx(db, () => {
195
- const time = now();
196
- addComment(db, ticket.id, actor.name, "completion", lines.join("\n\n"));
197
- db.prepare(
198
- `UPDATE tickets
199
- SET state_id = ?, implementation_summary = ?,
200
- implementation_artifact_path = ?, implementation_updates = ?, updated_at = ?
201
- WHERE id = ?`
202
- ).run(nextState.id, summary, implementationArtifactPath, updates, time, ticket.id);
203
- recordEvent(db, ticket.board_id, "agent_completed", ticket.id, actor.name, {
204
- from: ticket.state_name,
205
- next_state: nextState.name,
206
- to: nextState.name,
207
- pr_url: body.pr_url || "",
208
- actor_type: actor.type,
209
- actor_id: actor.id
210
- });
211
- reindexTicket(db, ticket.id);
212
- bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
213
- });
214
- return getContextPack(ticket.id, ctx, 1);
215
- }
216
-
217
- function contextBoardIdentity(innerBoard, ctx) {
218
- return {
219
- id: innerBoard.id,
220
- slug: innerBoard.slug,
221
- name: innerBoard.name,
222
- repo_path: innerBoard.system_path || ctx.board.repo_path || "",
223
- system_path: innerBoard.system_path || ctx.board.repo_path || "",
224
- default_branch: innerBoard.default_branch || "main"
225
- };
226
- }
227
-
228
- function buildContextPack(ticketId, ctx, depth = 1, options = {}, { includeProjectContext = false } = {}) {
229
- const { db, board, actor } = ctx;
230
- const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
231
- const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
232
- const includeRelatedFull = Boolean(options.include_related_full ?? options.includeRelatedFull);
233
- const ticket = ticketById(db, ticketId);
234
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
235
- const innerBoard = boardById(db, ticket.board_id);
236
- requireBoardAccess(actor, innerBoard);
237
-
238
- const labels = labelsForTicket(db, ticketId);
239
- const relations = relationRows(db, ticketId);
240
- const blockers = unresolvedBlockers(db, ticketId);
241
- const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
242
- const childTickets = childTicketsFor(db, ticketId).map((child) => ({
243
- ...child,
244
- labels: labelsForTicket(db, child.id),
245
- blockers: unresolvedBlockers(db, child.id)
246
- }));
247
-
248
- const relatedTickets = uniqueTickets([
249
- ...relations.map((relation) => relation.other_ticket),
250
- ...childTickets,
251
- ...(parentTicket ? [parentTicket] : [])
252
- ]);
253
-
254
- const pack = {
255
- ticket: capTicketFields({
256
- ...ticket,
257
- labels
258
- }, maxCharsPerField),
259
- board: contextBoardIdentity(innerBoard, ctx),
260
- parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
261
- child_tickets: childTickets.map((child) => ({
262
- ...capTicketFields(child, maxCharsPerField),
263
- child_count: childTicketsFor(db, child.id).length
264
- })),
265
- relations: relations.map((relation) => ({
266
- ...relation,
267
- other_ticket: includeRelatedFull
268
- ? capTicketFields({ ...relation.other_ticket, labels: labelsForTicket(db, relation.other_ticket.id) }, maxCharsPerField)
269
- : compactTicket(relation.other_ticket)
270
- })),
271
- blockers,
272
- related_tickets: relatedTickets.map((related) =>
273
- includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
274
- )
275
- };
276
-
277
- if (includeProjectContext) pack.board_manual = boardManual(innerBoard.id, ctx);
278
- return pack;
279
- }
280
-
281
- export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
282
- return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: false });
283
- }
284
-
285
- export function getContextPackFull(ticketId, ctx, depth = 1, options = {}) {
286
- return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: true });
287
- }
288
-
289
- export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
290
- const { db, board, actor } = ctx;
291
- const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 4000);
292
- const commentLimit = Math.min(nonNegativeInt(options.comment_limit ?? options.commentLimit, 5), 20);
293
- const ticket = ticketById(db, ticketId);
294
- if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
295
- const innerBoard = boardById(db, ticket.board_id);
296
- requireBoardAccess(actor, innerBoard);
297
-
298
- const labels = labelsForTicket(db, ticket.id).map((label) => label.name);
299
- const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
300
- const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
301
- const comments = db
302
- .prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at DESC LIMIT ?")
303
- .all(ticket.id, commentLimit)
304
- .reverse()
305
- .map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
306
-
307
- return {
308
- board: {
309
- id: innerBoard.id,
310
- slug: innerBoard.slug,
311
- name: innerBoard.name,
312
- repo_path: innerBoard.repo_path || innerBoard.system_path || "",
313
- system_path: innerBoard.system_path || innerBoard.repo_path || "",
314
- default_branch: innerBoard.default_branch || "main",
315
- agent_instructions: capString(innerBoard.agent_instructions || "", maxCharsPerField)
316
- },
317
- ticket: {
318
- id: ticket.id,
319
- ticket_id: ticket.id,
320
- number: ticket.number,
321
- title: ticket.title,
322
- type: ticket.type,
323
- priority: ticket.priority,
324
- state_id: ticket.state_id,
325
- state_name: ticket.state_name,
326
- state_role: ticket.state_role,
327
- description: capString(ticket.description || "", maxCharsPerField),
328
- acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
329
- ai_plan: capString(ticket.ai_plan || "", maxCharsPerField),
330
- plan_artifact_path: ticket.plan_artifact_path || "",
331
- implementation_summary: capString(ticket.implementation_summary || "", maxCharsPerField),
332
- implementation_artifact_path: ticket.implementation_artifact_path || "",
333
- implementation_updates: capString(ticket.implementation_updates || "", maxCharsPerField),
334
- labels
335
- },
336
- blockers: {
337
- can_start: blockers.length === 0,
338
- blockers
339
- },
340
- parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
341
- relevant_state_ids: relevantStateIds(db, innerBoard.id, ticket.state_id),
342
- recent_comments: comments
343
- };
344
- }
345
-
346
- function positiveInt(value, fallback) {
347
- const n = Number(value);
348
- return Number.isInteger(n) && n > 0 ? n : fallback;
349
- }
350
-
351
- function nonNegativeInt(value, fallback) {
352
- const n = Number(value);
353
- return Number.isInteger(n) && n >= 0 ? n : fallback;
354
- }
355
-
356
- function capString(value, maxChars) {
357
- const text = String(value || "");
358
- return maxChars && text.length > maxChars ? text.slice(0, maxChars) : text;
359
- }
360
-
361
- function capTicketFields(ticket, maxChars) {
362
- if (!maxChars) return ticket;
363
- const copy = { ...ticket };
364
- for (const field of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
365
- if (Object.hasOwn(copy, field)) copy[field] = capString(copy[field], maxChars);
366
- }
367
- return copy;
368
- }
369
-
370
- function extractAcceptance(description) {
371
- const match = String(description || "").match(/(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i);
372
- return match ? match[1].trim() : "";
373
- }
374
-
375
- function relevantStateIds(db, boardId, currentStateId) {
376
- const byRole = (role) => stateByRole(db, boardId, role)?.id || null;
377
- const byName = (name) => stateByName(db, boardId, name)?.id || null;
378
- return {
379
- current: currentStateId,
380
- current_role: stateById(db, currentStateId)?.role || null,
381
- ai_ready: byRole("ai_ready") || byName("AI Ready"),
382
- in_progress: byRole("in_progress") || byName("In Progress"),
383
- review: byRole("review") || byName("Review"),
384
- done: byRole("done") || byName("Done"),
385
- todo: byRole("todo") || byName("Todo")
386
- };
387
- }
@@ -1,109 +0,0 @@
1
- import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
-
4
- export const ORBIT_AGENTS_START = "<!--ORBIT-->";
5
- export const ORBIT_AGENTS_END = "<!--/ORBIT-->";
6
- export const ORBIT_AGENTS_CANONICAL_LINE =
7
- "Git is canonical for code. `SKILL-ORBIT.md` is canonical for Orbit/kanban/ticket/card workflow.";
8
-
9
- export function orbitAgentsSection() {
10
- return `${ORBIT_AGENTS_START}
11
- ${ORBIT_AGENTS_CANONICAL_LINE}
12
-
13
- For Orbit, kanban, board, lane, ticket, card, epic, blocker, claim, AI Ready, implementation fields, planning state, project memory, or handoff work: read \`SKILL-ORBIT.md\` first.
14
- Use Orbit API/MCP tools for tickets/cards; do not edit .orbit/board.db directly.
15
- ${ORBIT_AGENTS_END}\n`;
16
- }
17
-
18
- export function buildAgentsMd() {
19
- return `# AGENTS.md
20
-
21
- ${orbitAgentsSection()}`;
22
- }
23
-
24
- export function syncSkillOrbitMd(projectRoot, skillSrc) {
25
- const skillDest = resolve(projectRoot, "SKILL-ORBIT.md");
26
- if (!existsSync(skillSrc)) {
27
- return { ok: false, status: "missing_source", source: skillSrc, path: skillDest };
28
- }
29
- if (resolve(skillSrc) === skillDest) {
30
- return { ok: true, status: "same_file", source: skillSrc, path: skillDest };
31
- }
32
- copyFileSync(skillSrc, skillDest);
33
- return { ok: true, status: "written", source: skillSrc, path: skillDest };
34
- }
35
-
36
- /**
37
- * Write both Orbit helper files (SKILL-ORBIT.md + the AGENTS.md Orbit section)
38
- * into a project root. This is the shared core behind `orbit init` and the
39
- * web-UI "Generate AI helper files" checkbox, so the two flows stay identical.
40
- * Never throws — returns a result the caller can log or ignore.
41
- */
42
- export function generateHelperFiles(projectRoot, skillSrc, logger = console.log) {
43
- if (!existsSync(skillSrc)) {
44
- return { ok: false, status: "missing_source", source: skillSrc };
45
- }
46
- const skill = syncSkillOrbitMd(projectRoot, skillSrc);
47
- syncAgentsMd(projectRoot, {}, logger);
48
- return { ok: true, status: skill.status, skill };
49
- }
50
-
51
- export function syncAgentsMd(projectRoot, _options = {}, logger = console.log) {
52
- const agentsPath = resolve(projectRoot, "AGENTS.md");
53
- const section = orbitAgentsSection();
54
-
55
- if (!existsSync(agentsPath)) {
56
- writeFileSync(agentsPath, buildAgentsMd(), "utf8");
57
- logger(`Wrote ${agentsPath}`);
58
- return;
59
- }
60
-
61
- const current = readFileSync(agentsPath, "utf8");
62
- const startIndex = current.indexOf(ORBIT_AGENTS_START);
63
- const endIndex = current.indexOf(ORBIT_AGENTS_END);
64
-
65
- if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
66
- logger("AGENTS.md already includes Orbit instructions — left unchanged.");
67
- return;
68
- }
69
-
70
- writeFileSync(agentsPath, `${current.replace(/\s*$/, "\n\n")}${section}`, "utf8");
71
- logger(`Appended Orbit instructions to ${agentsPath}`);
72
- }
73
-
74
- function removeExactLegacyIntro(before) {
75
- const escaped = ORBIT_AGENTS_CANONICAL_LINE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
76
- const legacyIntro = new RegExp(`(?:^|\\n)[ \\t]*${escaped}[ \\t]*(?:\\r?\\n)?[ \\t]*$`);
77
- return before.replace(legacyIntro, (match) => (match.startsWith("\n") ? "\n" : ""));
78
- }
79
-
80
- function joinAfterAgentsRemoval(beforeRaw, afterRaw) {
81
- const before = removeExactLegacyIntro(beforeRaw).replace(/\s*$/, "");
82
- const after = afterRaw.replace(/^\s*/, "");
83
- if (before && after) return `${before}\n\n${after}`;
84
- if (before) return `${before}\n`;
85
- return after;
86
- }
87
-
88
- export function removeOrbitAgentsSection(projectRoot) {
89
- const agentsPath = resolve(projectRoot, "AGENTS.md");
90
- if (!existsSync(agentsPath)) {
91
- return { ok: true, removed: false, path: agentsPath, reason: "agents_md_missing" };
92
- }
93
-
94
- const current = readFileSync(agentsPath, "utf8");
95
- const startMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_START, "g"))];
96
- const endMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_END, "g"))];
97
- if (startMatches.length === 0 && endMatches.length === 0) {
98
- return { ok: true, removed: false, path: agentsPath, reason: "orbit_section_missing" };
99
- }
100
- if (startMatches.length !== 1 || endMatches.length !== 1 || endMatches[0].index <= startMatches[0].index) {
101
- return { ok: false, removed: false, path: agentsPath, reason: "ambiguous_orbit_section" };
102
- }
103
-
104
- const startIndex = startMatches[0].index;
105
- const endIndex = endMatches[0].index + ORBIT_AGENTS_END.length;
106
- const next = joinAfterAgentsRemoval(current.slice(0, startIndex), current.slice(endIndex));
107
- if (next !== current) writeFileSync(agentsPath, next, "utf8");
108
- return { ok: true, removed: next !== current, path: agentsPath };
109
- }
@@ -1,149 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, extname, isAbsolute, join, normalize, relative, sep } from "node:path";
3
- import { boardById, ticketById } from "./queries.js";
4
- import { requireBoardAccess } from "./auth.js";
5
- import { DISPATCH_RUNS_DIR } from "./paths.js";
6
- import { httpError } from "./util.js";
7
-
8
- const MAX_MARKDOWN_ARTIFACT_BYTES = 512 * 1024;
9
- const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
10
-
11
- function cleanPath(value) {
12
- const text = String(value || "").trim();
13
- if (!text || text.includes("\0")) throw httpError(400, "artifact_path_required");
14
- return text.replace(/\\/g, "/");
15
- }
16
-
17
- function ensureInside(path, root) {
18
- if (!root) return false;
19
- const normalizedRoot = normalize(root + sep);
20
- const normalizedPath = normalize(path);
21
- return normalizedPath === normalize(root) || normalizedPath.startsWith(normalizedRoot);
22
- }
23
-
24
- function pathAliases(path) {
25
- const aliases = new Set([path]);
26
- const mntDrive = path.match(/^\/mnt\/([a-z])\/(.+)$/i);
27
- if (mntDrive) {
28
- const drive = mntDrive[1].toLowerCase();
29
- aliases.add(`/${drive}/${mntDrive[2]}`);
30
- aliases.add(`${drive.toUpperCase()}:/${mntDrive[2]}`);
31
- }
32
- const slashDrive = path.match(/^\/([a-z])\/(.+)$/i);
33
- if (slashDrive) {
34
- const drive = slashDrive[1].toLowerCase();
35
- aliases.add(`/mnt/${drive}/${slashDrive[2]}`);
36
- aliases.add(`${drive.toUpperCase()}:/${slashDrive[2]}`);
37
- }
38
- const winDrive = path.match(/^([a-z]):\/(.+)$/i);
39
- if (winDrive) {
40
- const drive = winDrive[1].toLowerCase();
41
- aliases.add(`/mnt/${drive}/${winDrive[2]}`);
42
- aliases.add(`/${drive}/${winDrive[2]}`);
43
- }
44
- return [...aliases];
45
- }
46
-
47
- function isAbsoluteArtifactPath(path) {
48
- return isAbsolute(path) || /^[a-z]:\//i.test(path);
49
- }
50
-
51
- function artifactRoots(ctx) {
52
- const roots = [];
53
- const repoRoot = ctx.board.repo_path || ctx.board.system_path || ctx.board.project_root || "";
54
- if (repoRoot) roots.push(normalize(repoRoot));
55
- roots.push(normalize(DISPATCH_RUNS_DIR));
56
- if (ctx.board.db_path) {
57
- roots.push(normalize(join(dirname(ctx.board.db_path), "artifacts")));
58
- roots.push(normalize(join(dirname(ctx.board.db_path), "dispatch-runs")));
59
- }
60
- return [...new Set(roots.filter(Boolean))];
61
- }
62
-
63
- function resolveArtifactPath(ctx, requestedPath) {
64
- const raw = cleanPath(requestedPath);
65
- const roots = artifactRoots(ctx);
66
- if (roots.length === 0) throw httpError(400, "artifact_root_unavailable");
67
-
68
- const variants = pathAliases(raw);
69
- const absoluteVariants = variants.filter(isAbsoluteArtifactPath);
70
- const candidates = (absoluteVariants.length > 0
71
- ? absoluteVariants
72
- : roots.flatMap((root) => variants.map((variant) => join(root, variant))))
73
- .map((candidate) => normalize(candidate));
74
-
75
- const allowed = candidates.filter((candidate) => roots.some((root) => ensureInside(candidate, root)));
76
- const path = allowed.find((candidate) => existsSync(candidate)) || allowed[0];
77
- if (!path) throw httpError(400, "artifact_path_outside_allowed_roots");
78
- return { path, roots };
79
- }
80
-
81
- function safeArtifactFileName(value, fallback = "artifact.md") {
82
- const raw = cleanPath(value || fallback);
83
- const name = basename(raw).replace(/[^A-Za-z0-9._-]+/g, "-") || fallback;
84
- const ext = extname(name).toLowerCase();
85
- if (!MARKDOWN_EXTENSIONS.has(ext)) throw httpError(415, "unsupported_artifact_type");
86
- return name;
87
- }
88
-
89
- function boardArtifactRoot(ctx) {
90
- if (!ctx.board.db_path) throw httpError(400, "artifact_root_unavailable");
91
- return normalize(join(dirname(ctx.board.db_path), "artifacts"));
92
- }
93
-
94
- export function storeTicketMarkdownArtifact(ticketId, body, ctx) {
95
- const ticket = ticketById(ctx.db, ticketId);
96
- if (!ticket || ticket.board_id !== ctx.board.id) throw httpError(404, "ticket_not_found");
97
- requireBoardAccess(ctx.actor, boardById(ctx.db, ticket.board_id));
98
-
99
- const content = String(body?.content || "");
100
- const size = Buffer.byteLength(content, "utf8");
101
- if (size > MAX_MARKDOWN_ARTIFACT_BYTES) throw httpError(413, "artifact_too_large");
102
-
103
- const fileName = safeArtifactFileName(body?.filename || body?.path || "artifact.md");
104
- const root = boardArtifactRoot(ctx);
105
- const relativePath = `tickets/${ticket.id}/${fileName}`;
106
- const absolutePath = normalize(join(root, relativePath));
107
- if (!ensureInside(absolutePath, root)) throw httpError(400, "artifact_path_outside_allowed_roots");
108
-
109
- mkdirSync(dirname(absolutePath), { recursive: true });
110
- writeFileSync(absolutePath, content, "utf8");
111
-
112
- return {
113
- ticket_id: ticket.id,
114
- path: relativePath,
115
- absolute_path: absolutePath,
116
- size_bytes: size
117
- };
118
- }
119
-
120
- function displayPathFor(path, roots) {
121
- const root = roots
122
- .filter((candidate) => ensureInside(path, candidate))
123
- .sort((a, b) => normalize(b).length - normalize(a).length)[0];
124
- if (!root) return path;
125
- const rel = relative(root, path).replace(/\\/g, "/");
126
- return rel || path;
127
- }
128
-
129
- export function getTicketMarkdownArtifact(ticketId, requestedPath, ctx) {
130
- const ticket = ticketById(ctx.db, ticketId);
131
- if (!ticket || ticket.board_id !== ctx.board.id) throw httpError(404, "ticket_not_found");
132
- requireBoardAccess(ctx.actor, boardById(ctx.db, ticket.board_id));
133
-
134
- const { path, roots } = resolveArtifactPath(ctx, requestedPath);
135
- const ext = extname(path).toLowerCase();
136
- if (!MARKDOWN_EXTENSIONS.has(ext)) throw httpError(415, "unsupported_artifact_type");
137
- if (!existsSync(path)) throw httpError(404, "artifact_not_found");
138
- const stat = statSync(path);
139
- if (!stat.isFile()) throw httpError(400, "artifact_not_file");
140
- if (stat.size > MAX_MARKDOWN_ARTIFACT_BYTES) throw httpError(413, "artifact_too_large");
141
-
142
- return {
143
- ticket_id: ticket.id,
144
- path: displayPathFor(path, roots),
145
- absolute_path: path,
146
- size_bytes: stat.size,
147
- content: readFileSync(path, "utf8")
148
- };
149
- }