@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,1007 +0,0 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, extname, join, resolve } from "node:path";
4
- import { localOwnerActor } from "./auth.js";
5
- import { DISPATCH_RUNS_DIR } from "./paths.js";
6
- import { getContextPackFull } from "./agent.js";
7
- import { openBoardDb, getBoardByRegistryId, getBoardByRepoPath, getBoardBySlug, touchBoardActive } from "./registry.js";
8
- import { stateByName, stateByRole, ticketById, ticketByNumber, unresolvedBlockers } from "./queries.js";
9
- import { createRegistrySchema } from "./db.js";
10
- import { createComment, updateTicket } from "./tickets.js";
11
- import { id, normalizePath, slugify } from "./util.js";
12
-
13
- const DEFAULT_POLICY = "agent-safe";
14
- const DEFAULT_VERIFICATION_COMMANDS = [
15
- "npm test",
16
- "npm run build (when public/ or browser bundle assets change)",
17
- "node src/cli/orbit.js dispatch --help",
18
- "git diff --check",
19
- "git diff --cached --check before commit"
20
- ];
21
-
22
- function shortId() {
23
- return id().replace(/-/g, "").slice(0, 10);
24
- }
25
-
26
- function cleanName(value, fallback = "agent") {
27
- const cleaned = slugify(String(value || fallback)).replace(/^-+|-+$/g, "");
28
- return cleaned || fallback;
29
- }
30
-
31
- function resolveBoard(options) {
32
- if (options.board) {
33
- const row = getBoardByRegistryId(options.board) || getBoardBySlug(options.board);
34
- if (!row) throw new Error(`Board not found: ${options.board}`);
35
- return row;
36
- }
37
- const cwd = normalizePath(resolve(options.cwd));
38
- const row = getBoardByRepoPath(cwd);
39
- if (!row) throw new Error(`No Orbit board registered for ${cwd}; pass --board <slug-or-id>.`);
40
- return row;
41
- }
42
-
43
- function resolveTicket(db, boardId, ticketRef) {
44
- if (ticketRef === undefined || ticketRef === null || ticketRef === "") {
45
- throw new Error("--ticket <number-or-id> is required");
46
- }
47
- const text = String(ticketRef);
48
- const byNumber = /^\d+$/.test(text) ? ticketByNumber(db, boardId, Number(text)) : null;
49
- const ticket = byNumber || ticketById(db, text);
50
- if (!ticket || ticket.board_id !== boardId) throw new Error(`Ticket not found on board: ${ticketRef}`);
51
- if (ticket.archived_at) throw new Error(`Ticket #${ticket.number} is archived and cannot be dispatched.`);
52
- return ticket;
53
- }
54
-
55
- function parseTicketUrl(value) {
56
- if (!value) return null;
57
- let url;
58
- try {
59
- url = new URL(String(value));
60
- } catch {
61
- return null;
62
- }
63
- const hash = String(url.hash || "").replace(/^#/, "");
64
- const match = hash.match(/(?:^|\/)b\/([^/]+)\/t\/([^/?#]+)/);
65
- if (!match) return null;
66
- url.hash = "";
67
- url.search = "";
68
- url.pathname = url.pathname.replace(/\/+$/, "");
69
- return {
70
- serverUrl: url.toString().replace(/\/+$/, ""),
71
- board: decodeURIComponent(match[1]),
72
- ticket: decodeURIComponent(match[2])
73
- };
74
- }
75
-
76
- function remoteTarget(options) {
77
- const parsed = parseTicketUrl(options.ticketUrl || options.ticket);
78
- return {
79
- serverUrl: options.serverUrl || parsed?.serverUrl || process.env.ORBIT_SERVER_URL || process.env.ORBIT_API_URL || "",
80
- board: options.board || parsed?.board || process.env.ORBIT_DEFAULT_BOARD || "",
81
- ticket: parsed?.ticket || options.ticket || ""
82
- };
83
- }
84
-
85
- function resolveExecutable(command) {
86
- const executable = command || "hermes";
87
- if ((executable.includes("/") || executable.includes("\\")) && existsSync(executable)) return executable;
88
- const result = spawnSync("sh", ["-lc", `command -v ${q(executable)}`], { encoding: "utf8" });
89
- return result.status === 0 ? result.stdout.trim() : "";
90
- }
91
-
92
- function resolveCommandSpec(command) {
93
- const executable = resolveExecutable(command);
94
- if (!executable) return null;
95
- if (process.platform === "win32" && existsSync(executable)) {
96
- const extension = extname(executable).toLowerCase();
97
- if (!extension || extension === ".js") {
98
- try {
99
- const firstLine = readFileSync(executable, "utf8").split(/\r?\n/, 1)[0] || "";
100
- if (/^#!.*\bnode\b/i.test(firstLine)) {
101
- return { command: process.execPath, argsPrefix: [executable], display: executable };
102
- }
103
- } catch {
104
- // Fall through to direct execution for non-text executables.
105
- }
106
- }
107
- }
108
- return { command: executable, argsPrefix: [], display: executable };
109
- }
110
-
111
- function preflightHermes(options, profile) {
112
- const hermes = resolveCommandSpec(options.hermesBin || "hermes");
113
- if (!hermes) {
114
- throw new Error(
115
- `Hermes binary not found: ${options.hermesBin || "hermes"}. Install Hermes or pass --hermes-bin <path>. ` +
116
- "For prepare-only handoff without spawning, rerun with --no-spawn."
117
- );
118
- }
119
- const result = spawnSync(hermes.command, [...hermes.argsPrefix, "-p", profile, "--help"], { encoding: "utf8" });
120
- if (result.error) {
121
- throw new Error(`Hermes preflight failed for profile ${profile}: ${result.error.message}. Try --no-spawn for prepare-only.`);
122
- }
123
- if (result.status !== 0) {
124
- const detail = (result.stderr || result.stdout || "").trim();
125
- throw new Error(
126
- `Hermes preflight failed for profile ${profile}${detail ? `: ${detail}` : ""}. ` +
127
- "Check the profile name or rerun with --no-spawn for prepare-only."
128
- );
129
- }
130
- return hermes;
131
- }
132
-
133
- function resolveRepoRoot(options, boardRow) {
134
- const cwd = normalizePath(resolve(options.cwd));
135
- if (options.cwdProvided || existsSync(join(cwd, ".git"))) return cwd;
136
- if (boardRow.repo_path && existsSync(join(boardRow.repo_path, ".git"))) return boardRow.repo_path;
137
- return cwd;
138
- }
139
-
140
- function commandOutput(command, args, options = {}) {
141
- const executable = command === "git" ? findExecutable("git") || command : command;
142
- const result = spawnSync(executable, args, { encoding: "utf8", ...options });
143
- if (result.error) throw result.error;
144
- if (result.status !== 0) {
145
- const detail = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
146
- throw new Error(`${command} ${args.join(" ")} failed${detail ? `\n${detail}` : ""}`);
147
- }
148
- return String(result.stdout || "").trim();
149
- }
150
-
151
- function optionalCommandOutput(command, args, options = {}) {
152
- try {
153
- return commandOutput(command, args, options);
154
- } catch {
155
- return "";
156
- }
157
- }
158
-
159
- function normalizeVerificationCommands(commands) {
160
- const values = Array.isArray(commands) ? commands : commands ? [commands] : [];
161
- const cleaned = values.map((value) => String(value || "").trim()).filter(Boolean);
162
- return cleaned.length ? cleaned : [...DEFAULT_VERIFICATION_COMMANDS];
163
- }
164
-
165
- const REDACTED_URL_PRESENT = "[redacted-url-present]";
166
- const URL_ENV_KEYS = new Set(["ORBIT_API_URL", "ORBIT_SERVER_URL"]);
167
-
168
- function sanitizeUrlBearingValue(value) {
169
- if (value === undefined || value === null || value === "") return "";
170
- return REDACTED_URL_PRESENT;
171
- }
172
-
173
- function sanitizeEnvironmentValue(key, value) {
174
- if (URL_ENV_KEYS.has(key)) return sanitizeUrlBearingValue(value);
175
- return value;
176
- }
177
-
178
- function collectEnvironmentVariance() {
179
- const envKeys = ["HOME", "PROJECT_ROOT", "DATA_DIR", "ORBIT_MODE", "ORBIT_API_URL", "ORBIT_DEFAULT_BOARD", "ORBIT_SERVER_URL"];
180
- const env = {};
181
- for (const key of envKeys) {
182
- if (process.env[key] !== undefined) env[key] = sanitizeEnvironmentValue(key, process.env[key]);
183
- }
184
- return {
185
- platform: process.platform,
186
- node: process.version,
187
- cwd: process.cwd(),
188
- path_format: process.platform === "win32" ? "windows" : "posix",
189
- env
190
- };
191
- }
192
-
193
- function buildRunRecordJson({
194
- runId,
195
- status,
196
- boardRow,
197
- ticket,
198
- profile,
199
- policyName,
200
- repoRoot,
201
- worktreePath,
202
- branchName,
203
- baseCommit,
204
- handoffPath,
205
- policyBin,
206
- child,
207
- hermesArgs,
208
- verificationCommands,
209
- options,
210
- serverUrl,
211
- environmentVariance
212
- }) {
213
- return {
214
- schema: "orbit.dispatch.run.v1",
215
- run_id: runId,
216
- ticket_id: ticket.id,
217
- ticket_number: ticket.number,
218
- board_id: boardRow.id,
219
- board_slug: boardRow.slug,
220
- profile,
221
- prompt_path: handoffPath,
222
- worktree: worktreePath || repoRoot,
223
- repo_root: repoRoot,
224
- branch: branchName || optionalCommandOutput("git", ["-C", repoRoot, "branch", "--show-current"]),
225
- base_commit: baseCommit,
226
- process_id: child?.pid || null,
227
- policy: policyName || "none",
228
- status,
229
- verification_commands: verificationCommands,
230
- commit_shas: [],
231
- residual_risks: [],
232
- server_url: sanitizeUrlBearingValue(serverUrl),
233
- transport: options.remote || options.serverUrl || options.ticketUrl ? "remote" : "local",
234
- mode: options.noSpawn ? "prepare-only" : "spawn",
235
- command: `${options.hermesBin || "hermes"} ${hermesArgs.map((arg) => (arg.includes(" ") ? q(arg) : arg)).join(" ")}`,
236
- created_at: new Date().toISOString(),
237
- environment_variance: environmentVariance
238
- };
239
- }
240
-
241
- function findExecutable(name) {
242
- const filteredPath = String(process.env.PATH || "")
243
- .split(process.platform === "win32" ? ";" : ":")
244
- .filter((entry) => entry && !/(^|[/\\])policy-bin$/i.test(entry))
245
- .join(process.platform === "win32" ? ";" : ":");
246
- const result = spawnSync("sh", ["-lc", `command -v ${name}`], { encoding: "utf8", env: { ...process.env, PATH: filteredPath } });
247
- return result.status === 0 ? result.stdout.trim() : "";
248
- }
249
-
250
- function q(value) {
251
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
252
- }
253
-
254
- function createWrapper(path, content) {
255
- writeFileSync(path, content, "utf8");
256
- chmodSync(path, 0o755);
257
- }
258
-
259
- function createWindowsCmdWrapper(path, blockedMessage) {
260
- writeFileSync(
261
- `${path}.cmd`,
262
- `@echo off\r\necho ${blockedMessage} 1>&2\r\nexit /b 126\r\n`,
263
- "utf8"
264
- );
265
- chmodSync(`${path}.cmd`, 0o755);
266
- }
267
-
268
- function createPolicyBin(runDir, policyName) {
269
- if (!policyName || policyName === "none") return null;
270
- const binDir = join(runDir, "policy-bin");
271
- mkdirSync(binDir, { recursive: true });
272
-
273
- const realGit = findExecutable("git") || "/usr/bin/git";
274
- const realNpm = findExecutable("npm") || "/usr/bin/npm";
275
- const realPnpm = findExecutable("pnpm") || "/usr/bin/pnpm";
276
- const realYarn = findExecutable("yarn") || "/usr/bin/yarn";
277
- const realRm = findExecutable("rm") || "/usr/bin/rm";
278
-
279
- createWrapper(
280
- join(binDir, "git"),
281
- `#!/bin/sh
282
- cmd="\${1:-}"
283
- shift 2>/dev/null || true
284
- block() {
285
- echo "Blocked by Orbit ${policyName} policy: git $cmd $*" >&2
286
- exit 126
287
- }
288
- case "$cmd" in
289
- status|diff|log|show|add|rev-parse|describe|ls-files|grep|merge-base)
290
- exec ${q(realGit)} "$cmd" "$@"
291
- ;;
292
- commit)
293
- for arg in "$@"; do
294
- case "$arg" in --amend|--fixup=*|--squash=*) block "$@" ;; esac
295
- done
296
- exec ${q(realGit)} "$cmd" "$@"
297
- ;;
298
- branch)
299
- for arg in "$@"; do
300
- case "$arg" in -d|-D|--delete|-m|-M|--move) block "$@" ;; esac
301
- done
302
- exec ${q(realGit)} "$cmd" "$@"
303
- ;;
304
- push|reset|clean|rebase|merge|checkout|switch|restore|tag|cherry-pick|worktree)
305
- block "$@"
306
- ;;
307
- *)
308
- echo "Blocked by Orbit ${policyName} policy: git $cmd (allowed: status, diff, log, show, add, commit, safe branch inspection/creation, rev-parse, describe, ls-files, grep, merge-base)" >&2
309
- exit 126
310
- ;;
311
- esac
312
- `
313
- );
314
- if (process.platform === "win32") {
315
- createWindowsCmdWrapper(join(binDir, "git"), `Blocked by Orbit ${policyName} policy: git %~1`);
316
- }
317
-
318
- const packageWrapper = (realCommand, name) => `#!/bin/sh
319
- cmd="\${1:-}"
320
- script="\${2:-}"
321
- case "$cmd" in
322
- test)
323
- exec ${q(realCommand)} "$@"
324
- ;;
325
- run)
326
- case "$script" in
327
- test|test:*|build|build:*|lint|lint:*|check|check:*|typecheck|typecheck:*)
328
- exec ${q(realCommand)} "$@"
329
- ;;
330
- esac
331
- ;;
332
- publish|version|deploy|release)
333
- echo "Blocked by Orbit ${policyName} policy: ${name} $cmd" >&2
334
- exit 126
335
- ;;
336
- esac
337
- echo "Blocked by Orbit ${policyName} policy: ${name} $cmd (allowed package commands: test, run test/build/lint/check/typecheck)" >&2
338
- exit 126
339
- `;
340
-
341
- createWrapper(join(binDir, "npm"), packageWrapper(realNpm, "npm"));
342
- createWrapper(join(binDir, "pnpm"), packageWrapper(realPnpm, "pnpm"));
343
- createWrapper(join(binDir, "yarn"), packageWrapper(realYarn, "yarn"));
344
- if (process.platform === "win32") {
345
- createWindowsCmdWrapper(join(binDir, "npm"), `Blocked by Orbit ${policyName} policy: npm %1 (allowed package commands: test)`);
346
- createWindowsCmdWrapper(join(binDir, "pnpm"), `Blocked by Orbit ${policyName} policy: pnpm %1 (allowed package commands: test)`);
347
- createWindowsCmdWrapper(join(binDir, "yarn"), `Blocked by Orbit ${policyName} policy: yarn %1 (allowed package commands: test)`);
348
- }
349
-
350
- createWrapper(
351
- join(binDir, "docker"),
352
- `#!/bin/sh
353
- echo "Blocked by Orbit ${policyName} policy: Docker requires explicit human approval." >&2
354
- exit 126
355
- `
356
- );
357
- if (process.platform === "win32") {
358
- createWindowsCmdWrapper(join(binDir, "docker"), `Blocked by Orbit ${policyName} policy: Docker requires explicit human approval.`);
359
- }
360
-
361
- for (const name of ["kubectl", "vercel", "netlify", "fly", "railway", "scp", "rsync", "ssh", "sudo", "gh", "curl", "wget", "aws", "gcloud", "az"]) {
362
- createWrapper(
363
- join(binDir, name),
364
- `#!/bin/sh
365
- echo "Blocked by Orbit ${policyName} policy: ${name} is outside this autonomous run." >&2
366
- exit 126
367
- `
368
- );
369
- }
370
-
371
- createWrapper(
372
- join(binDir, "rm"),
373
- `#!/bin/sh
374
- for arg in "$@"; do
375
- case "$arg" in
376
- -r|-R|-f|-rf|-fr|-Rf|-fR|--recursive|--force)
377
- echo "Blocked by Orbit ${policyName} policy: recursive/force rm requires human approval." >&2
378
- exit 126
379
- ;;
380
- esac
381
- done
382
- exec ${q(realRm)} "$@"
383
- `
384
- );
385
-
386
- return binDir;
387
- }
388
-
389
- function buildHandoff({ boardRow, context, profile, repoRoot, worktreePath, branchName, baseCommit, policyName, runId, serverUrl, verificationCommands }) {
390
- const { ticket, board, board_manual: manual, parent_ticket: parent, child_tickets: children, blockers } = context;
391
- const boardEntries = (manual.entries || [])
392
- .slice(0, 12)
393
- .map((entry) => `- ${entry.type}: ${entry.title}\n ${entry.body}`)
394
- .join("\n");
395
- const blockerText = blockers.length
396
- ? blockers.map((blocker) => `- #${blocker.number} ${blocker.title} (${blocker.state_name})`).join("\n")
397
- : "None.";
398
- const childText = children.length
399
- ? children.map((child) => `- #${child.number} ${child.title} (${child.state_name})`).join("\n")
400
- : "None.";
401
- const recentComments = (context.recent_comments || [])
402
- .slice(-5)
403
- .map((comment) => `- ${comment.author || "unknown"} (${comment.kind || "comment"}): ${comment.body || ""}`)
404
- .join("\n");
405
- const verificationText = verificationCommands.map((command) => `- ${command}`).join("\n");
406
-
407
- return `# Orbit Agent Handoff
408
-
409
- Run ID: ${runId}
410
- Agent profile: ${profile}
411
- Board: ${board.slug} — ${board.name}
412
- Ticket: #${ticket.number} — ${ticket.title}
413
- Ticket id: ${ticket.id}
414
- Ticket type: ${ticket.type}
415
- Initial state: ${ticket.state_name}
416
- Repository root: ${repoRoot}
417
- Worktree: ${worktreePath || repoRoot}
418
- Branch: ${branchName || "current branch"}
419
- Base commit: ${baseCommit || "unknown"}
420
- ${serverUrl ? `Orbit server: ${serverUrl}` : ""}
421
-
422
- ## Mission
423
- Implement the ticket faithfully. Treat this ticket as the source of truth for communication between the human, orchestrator, and dispatched agent.
424
-
425
- ## Read first
426
- 1. AGENTS.md in the repository.
427
- 2. SKILL-ORBIT.md in the repository.
428
- 3. This linked markdown handoff.
429
- 4. The ticket description, terse visible AI Plan / Work Summary, Revisions, blockers, parent/child cards, and board Journal entries below.
430
-
431
- ## Ticket description
432
- ${ticket.description || "(No description.)"}
433
-
434
- ## Human-visible card fields
435
- AI Plan: ${ticket.ai_plan || "(Empty.)"}
436
-
437
- Linked plan artifact: ${ticket.plan_artifact_path || "(None.)"}
438
-
439
- Work Summary: ${ticket.implementation_summary || "(Empty.)"}
440
-
441
- Linked implementation artifact: ${ticket.implementation_artifact_path || "(None.)"}
442
-
443
- Revisions: ${ticket.implementation_updates || "(Empty.)"}
444
-
445
- ## Board agent instructions
446
- ${board.agent_instructions || "(No board-level agent instructions.)"}
447
-
448
- ## Board journal: lessons and decisions
449
- Board Journal entries are the most important code, pattern, project, and workflow context loaded for every agent working with this board. Treat decisions and lessons as project constraints, not persona or roleplay instructions.
450
-
451
- ${boardEntries || "(No active board Journal entries.)"}
452
-
453
- ## Parent ticket
454
- ${parent ? `#${parent.number} ${parent.title} (${parent.state_name})` : "None."}
455
-
456
- ## Child tickets
457
- ${childText}
458
-
459
- ## Unresolved blockers
460
- ${blockerText}
461
-
462
- ${recentComments ? `## Recent ticket comments\n${recentComments}\n` : ""}
463
- ## Scope boundaries
464
- - Keep changes local to this repository/worktree.
465
- - Do not push, deploy, publish, or open/merge PRs.
466
- - Do not run Docker unless the human operator explicitly approves it outside this autonomous run.
467
- - Do not expose secrets, tokens, credentials, private keys, or connection strings.
468
- - Prefer small, reviewable commits.
469
-
470
- ## Autonomous policy: ${policyName}
471
- Allowed without asking: read/search files, inspect git status/diff/log, edit files for this ticket, run package test/build/lint/check/typecheck commands, and create a local git commit.
472
- Blocked or requires explicit human approval: Docker, git push, deploy/publish/release commands, destructive git operations (reset/clean/rebase/force checkout/worktree removal), package install/publish/version commands, recursive/force filesystem deletion, SSH/SCP/rsync, cloud CLIs, GitHub CLI, direct curl/wget network calls, sudo, and secret exfiltration.
473
-
474
- ## Declared verification commands
475
- Run the commands that apply before handoff. If a conditional command does not apply, say why in the completion comment or Work Summary.
476
-
477
- ${verificationText}
478
-
479
- ## Environment variance checklist
480
- - HOME / DATA_DIR / PROJECT_ROOT / ORBIT_* inherited env vars
481
- - Existing local board DB/files and central registry state
482
- - Port availability when touching serve/SSE/API flows
483
- - OS path format and shell differences
484
- - Accidental use of real user state in tests or dispatch artifacts
485
-
486
- ## Completion protocol
487
- When done:
488
- 1. Run the relevant automated tests and any focused manual/API sanity checks.
489
- 2. Commit the work locally on the dispatch branch.
490
- 3. Put a terse, human-readable result summary in Work Summary (implementation_summary).
491
- 4. If full detail matters, write a markdown report artifact and link it with implementation_artifact_path.
492
- 5. Leave Revisions alone unless this run revises earlier implementation; promote important code/pattern/project lessons and decisions to board Journal entries.
493
- 6. Add a compact completion comment/run receipt on the ticket.
494
- 7. Move the ticket to Review, not Done, unless the human operator explicitly requested auto-completion.
495
- `;
496
- }
497
-
498
- function buildShortPrompt({ boardRow, ticket, profile, serverUrl }) {
499
- return [
500
- `You are ${profile}, dispatched by Orbit.`,
501
- `Work board ${boardRow.slug}, ticket #${ticket.number}: ${ticket.title}.`,
502
- "Read AGENTS.md and SKILL-ORBIT.md in this repository first.",
503
- "The full generated handoff is linked from the ticket as a markdown artifact; treat the Orbit ticket plus linked artifact as source of truth.",
504
- serverUrl ? `Orbit server URL: ${serverUrl}` : "Use local Orbit/MCP/API access available from this repo.",
505
- "Follow the autonomous policy in the handoff. Do not push, deploy, publish, run Docker, or expose secrets.",
506
- "When complete, commit locally, update a terse Work Summary plus any linked markdown report on the ticket, use Revisions only for post-initial corrections, comment the run receipt, and move the ticket to Review."
507
- ].join("\n");
508
- }
509
-
510
- function buildPlanExcerpt({ ticket, profile, handoffPath }) {
511
- return [
512
- `- ${profile} dispatched to work #${ticket.number}: ${ticket.title}.`,
513
- `- Full generated handoff is linked as markdown: ${handoffPath}`,
514
- "- Follow blockers, scope boundaries, autonomous policy, and declared verification in that artifact.",
515
- "- Keep AI Plan and Work Summary terse; put full detail in linked markdown/report artifacts."
516
- ].join("\n");
517
- }
518
-
519
- function ensureWorktree({ repoRoot, boardSlug, ticketNumber, profile, branchName, worktreePath }) {
520
- commandOutput("git", ["-C", repoRoot, "rev-parse", "--show-toplevel"]);
521
- const runBranch = branchName || `orbit/${cleanName(boardSlug)}-${ticketNumber}-${cleanName(profile)}-${shortId()}`;
522
- const runWorktree = worktreePath || join(repoRoot, ".worktrees", `${cleanName(boardSlug)}-${ticketNumber}-${cleanName(profile)}-${shortId()}`);
523
- mkdirSync(resolve(runWorktree, ".."), { recursive: true });
524
- if (!existsSync(runWorktree)) {
525
- commandOutput("git", ["-C", repoRoot, "worktree", "add", "-b", runBranch, runWorktree, "HEAD"]);
526
- }
527
- return { branchName: runBranch, worktreePath: runWorktree };
528
- }
529
-
530
- function remoteRunId(ticket, profile) {
531
- return `orbit-${ticket.number}-${profile}-${shortId()}`;
532
- }
533
-
534
- function remoteRunDir(repoRoot, runId) {
535
- return join(repoRoot, ".orbit", "dispatch-runs", runId);
536
- }
537
-
538
- function remoteWorktreePath(repoRoot, runId) {
539
- return join(dirname(repoRoot), ".worktrees", basename(repoRoot), runId);
540
- }
541
-
542
- function contextFromDispatchPacket(packet, blockers) {
543
- const blockerList = Array.isArray(blockers?.blockers) ? blockers.blockers : packet.blockers?.blockers || [];
544
- return {
545
- ticket: packet.ticket,
546
- board: packet.board,
547
- board_manual: { entries: [] },
548
- parent_ticket: packet.parent_ticket || null,
549
- child_tickets: [],
550
- blockers: blockerList,
551
- recent_comments: packet.recent_comments || []
552
- };
553
- }
554
-
555
- async function createRemoteClient(options, target) {
556
- if (options.remoteClient) return options.remoteClient;
557
- const { createHttpOrbitClient } = await import("../mcp/orbit-client.js");
558
- return createHttpOrbitClient({
559
- ...process.env,
560
- ORBIT_MODE: "remote",
561
- ORBIT_API_URL: target.serverUrl,
562
- ORBIT_DEFAULT_BOARD: target.board
563
- });
564
- }
565
-
566
- async function dispatchTicketRemote(options) {
567
- const target = remoteTarget(options);
568
- if (!target.serverUrl) throw new Error("Remote dispatch requires --server-url or a hosted Orbit ticket URL.");
569
- if (!target.board) throw new Error("Remote dispatch requires --board or a hosted Orbit ticket URL containing /b/<slug>.");
570
- if (!target.ticket) throw new Error("Remote dispatch requires --ticket or a hosted Orbit ticket URL containing /t/<ticket>.");
571
-
572
- const profile = options.profile || "agent";
573
- const policyName = options.policy === undefined ? DEFAULT_POLICY : options.policy;
574
- const verificationCommands = normalizeVerificationCommands(options.verifyCommands);
575
- const remoteClient = await createRemoteClient(options, target);
576
- await remoteClient.boardSetActive?.({ slug: target.board });
577
-
578
- let packet;
579
- if (/^\d+$/.test(String(target.ticket))) {
580
- const found = await remoteClient.readTicket({ board_slug: target.board, number: Number(target.ticket) });
581
- const ticketId = found.ticket?.id || found.results?.[0]?.id || found.id;
582
- if (!ticketId) throw new Error(`Ticket not found on remote board: ${target.ticket}`);
583
- packet = await remoteClient.getAgentDispatchPacket({ board_slug: target.board, ticket_id: ticketId, comment_limit: 5 });
584
- } else {
585
- packet = await remoteClient.getAgentDispatchPacket({ board_slug: target.board, ticket_id: target.ticket, comment_limit: 5 });
586
- }
587
- const blockers = await remoteClient.getTicketBlockers({ board_slug: target.board, ticket_id: packet.ticket.id });
588
- const blockerList = Array.isArray(blockers?.blockers) ? blockers.blockers : [];
589
- if (blockerList.length && !options.force) {
590
- throw new Error(`Ticket #${packet.ticket.number} is blocked: ${blockerList.map((b) => `#${b.number} ${b.title}`).join(", ")}`);
591
- }
592
-
593
- const boardRow = {
594
- id: packet.board.id,
595
- slug: packet.board.slug,
596
- name: packet.board.name,
597
- repo_path: packet.board.repo_path || packet.board.system_path || "",
598
- default_branch: packet.board.default_branch || "main"
599
- };
600
- const ticket = packet.ticket;
601
- const repoRoot = resolveRepoRoot(options, boardRow);
602
-
603
- if (options.dryRun) {
604
- return {
605
- dry_run: true,
606
- no_spawn: Boolean(options.noSpawn),
607
- run_id: null,
608
- board: { id: boardRow.id, slug: boardRow.slug, name: boardRow.name },
609
- ticket: { id: ticket.id, number: ticket.number, title: ticket.title },
610
- profile,
611
- policy: policyName || "none",
612
- repo_root: repoRoot,
613
- worktree_path: repoRoot,
614
- branch: options.branch || "",
615
- handoff_path: null,
616
- policy_bin: null,
617
- pid: null,
618
- spawned: false,
619
- prompt: "",
620
- run_record: "",
621
- run_record_path: null,
622
- verification_commands: verificationCommands,
623
- remote: true
624
- };
625
- }
626
-
627
- const inProgressId = packet.relevant_state_ids?.in_progress;
628
- if (!options.noSpawn && !inProgressId) throw new Error("Remote board is missing an In Progress state id in the dispatch packet.");
629
- const hermesExecutable = options.noSpawn ? null : preflightHermes(options, profile);
630
- const runId = remoteRunId(ticket, profile);
631
- const runDir = remoteRunDir(repoRoot, runId);
632
- const baseCommit = optionalCommandOutput("git", ["-C", repoRoot, "rev-parse", "HEAD"]);
633
- const environmentVariance = collectEnvironmentVariance();
634
- let branchName = options.branch || "";
635
- let worktreePath = "";
636
-
637
- if (options.worktree) {
638
- const worktree = ensureWorktree({
639
- repoRoot,
640
- boardSlug: boardRow.slug,
641
- ticketNumber: ticket.number,
642
- profile,
643
- branchName: options.branch || `orbit/${ticket.number}-${cleanName(profile)}-remote-${shortId()}`,
644
- worktreePath: options.worktreePath || remoteWorktreePath(repoRoot, runId)
645
- });
646
- branchName = worktree.branchName;
647
- worktreePath = worktree.worktreePath;
648
- }
649
-
650
- mkdirSync(runDir, { recursive: true });
651
- const policyBin = createPolicyBin(runDir, policyName);
652
- const context = contextFromDispatchPacket(packet, blockers);
653
- const displayServerUrl = sanitizeUrlBearingValue(target.serverUrl);
654
- const handoff = `${buildHandoff({
655
- boardRow,
656
- context,
657
- profile,
658
- repoRoot,
659
- worktreePath,
660
- branchName,
661
- baseCommit,
662
- policyName,
663
- runId,
664
- serverUrl: displayServerUrl,
665
- verificationCommands
666
- })}\n## Remote Orbit environment for spawned agents\n- ORBIT_MODE=remote\n- ORBIT_DEFAULT_BOARD=${boardRow.slug}\n- ORBIT_API_URL is provided by dispatch when spawning; URL-bearing values are redacted from run records.\n`;
667
- const handoffPath = join(runDir, "handoff.md");
668
- writeFileSync(handoffPath, handoff, "utf8");
669
-
670
- const prompt = buildShortPrompt({ boardRow, ticket, profile, serverUrl: displayServerUrl });
671
- const hermesArgs = [];
672
- if (profile) hermesArgs.push("-p", profile);
673
- if (options.yolo) hermesArgs.push("--yolo");
674
- hermesArgs.push("chat", "-q", prompt);
675
- const runRecordPath = join(runDir, "run-record.json");
676
- const writeRunRecordFile = options.writeRunRecordFile || ((path, runRecordJson) => {
677
- writeFileSync(path, `${JSON.stringify(runRecordJson, null, 2)}\n`, "utf8");
678
- });
679
- const writeRunRecordJson = (status, childProcess = null) => {
680
- const runRecordJson = buildRunRecordJson({
681
- runId,
682
- status,
683
- boardRow,
684
- ticket,
685
- profile,
686
- policyName,
687
- repoRoot,
688
- worktreePath,
689
- branchName,
690
- baseCommit,
691
- handoffPath,
692
- policyBin,
693
- child: childProcess,
694
- hermesArgs,
695
- verificationCommands,
696
- options: { ...options, remote: true },
697
- serverUrl: target.serverUrl,
698
- environmentVariance
699
- });
700
- writeRunRecordFile(runRecordPath, runRecordJson);
701
- return runRecordJson;
702
- };
703
-
704
- let status = "prepared";
705
- writeRunRecordJson(status, null);
706
- const planSummary = buildPlanExcerpt({ ticket, profile, handoffPath });
707
- let linkedHandoffPath = handoffPath;
708
- if (typeof remoteClient.uploadMarkdownArtifact === "function") {
709
- const uploaded = await remoteClient.uploadMarkdownArtifact({
710
- ticket_id: ticket.id,
711
- board_slug: boardRow.slug,
712
- filename: `${runId}-handoff.md`,
713
- source_path: handoffPath,
714
- content: readFileSync(handoffPath, "utf8")
715
- });
716
- if (uploaded?.path) linkedHandoffPath = uploaded.path;
717
- }
718
- const ticketPatch = { ticket_id: ticket.id, board_slug: boardRow.slug, ai_plan: planSummary, plan_artifact_path: linkedHandoffPath };
719
- if (!options.noSpawn) ticketPatch.state_id = inProgressId;
720
- await remoteClient.updateTicket(ticketPatch);
721
-
722
- const cwd = worktreePath || repoRoot;
723
- const env = {
724
- ...process.env,
725
- ORBIT_MODE: "remote",
726
- ORBIT_API_URL: target.serverUrl,
727
- ORBIT_DEFAULT_BOARD: boardRow.slug,
728
- ORBIT_DISPATCH_RUN_ID: runId,
729
- ORBIT_BOARD: boardRow.slug,
730
- ORBIT_TICKET: String(ticket.number),
731
- ORBIT_TICKET_ID: ticket.id,
732
- ORBIT_HANDOFF_PATH: handoffPath,
733
- ORBIT_AUTONOMOUS_POLICY: policyName || "none",
734
- ...(policyBin ? { PATH: `${policyBin}:${process.env.PATH || ""}` } : {})
735
- };
736
-
737
- let child = null;
738
- if (!options.noSpawn) {
739
- child = spawn(hermesExecutable.command, [...hermesExecutable.argsPrefix, ...hermesArgs], {
740
- cwd,
741
- env,
742
- detached: !options.foreground,
743
- stdio: options.foreground ? "inherit" : "ignore"
744
- });
745
- if (!options.foreground) child.unref();
746
- status = "launched";
747
- writeRunRecordJson(status, child);
748
- }
749
-
750
- const runRecord = [
751
- `Orbit remote dispatch run ${options.noSpawn ? "prepared" : "started"}.`,
752
- `- run_id: ${runId}`,
753
- `- profile: ${profile}`,
754
- `- status: ${status}`,
755
- `- transport: remote hosted board`,
756
- `- policy: ${policyName || "none"}`,
757
- `- ticket: #${ticket.number} ${ticket.title}`,
758
- `- branch: ${branchName || "current branch"}`,
759
- `- base_commit: ${baseCommit || "unknown"}`,
760
- `- worktree: ${worktreePath || repoRoot}`,
761
- `- handoff: ${handoffPath}`,
762
- linkedHandoffPath !== handoffPath ? `- hosted_handoff: ${linkedHandoffPath}` : null,
763
- `- run_record_json: ${runRecordPath}`,
764
- `- policy_bin: ${policyBin || "none"}`,
765
- `- pid: ${child?.pid || "not spawned"}`,
766
- `- verification_commands: ${verificationCommands.join("; ")}`,
767
- `- command: ${options.hermesBin || "hermes"} ${hermesArgs.map((arg) => (arg.includes(" ") ? q(arg) : arg)).join(" ")}`,
768
- options.noSpawn ? "- mode: prepare-only; no agent spawned and ticket state left unchanged" : "- mode: spawned; ticket moved to In Progress"
769
- ].filter(Boolean).join("\n");
770
-
771
- await remoteClient.addComment({ ticket_id: ticket.id, board_slug: boardRow.slug, author: "orbit dispatch", kind: "agent_note", body: runRecord });
772
-
773
- return {
774
- dry_run: false,
775
- no_spawn: Boolean(options.noSpawn),
776
- run_id: runId,
777
- board: { id: boardRow.id, slug: boardRow.slug, name: boardRow.name },
778
- ticket: { id: ticket.id, number: ticket.number, title: ticket.title },
779
- profile,
780
- policy: policyName || "none",
781
- repo_root: repoRoot,
782
- worktree_path: worktreePath || repoRoot,
783
- branch: branchName || "",
784
- handoff_path: handoffPath,
785
- linked_handoff_path: linkedHandoffPath,
786
- policy_bin: policyBin,
787
- pid: child?.pid || null,
788
- spawned: Boolean(child),
789
- prompt,
790
- run_record: runRecord,
791
- run_record_path: runRecordPath,
792
- verification_commands: verificationCommands,
793
- remote: true
794
- };
795
- }
796
-
797
- export async function dispatchTicketAsync(options) {
798
- if (options.remote || options.serverUrl || options.ticketUrl || parseTicketUrl(options.ticket)) {
799
- return dispatchTicketRemote({ ...options, remote: true });
800
- }
801
- return dispatchTicket(options);
802
- }
803
-
804
- export function dispatchTicket(options) {
805
- const profile = options.profile || "agent";
806
- const policyName = options.policy === undefined ? DEFAULT_POLICY : options.policy;
807
- const verificationCommands = normalizeVerificationCommands(options.verifyCommands);
808
- createRegistrySchema();
809
- const actor = localOwnerActor();
810
- const boardRow = resolveBoard(options);
811
- const db = openBoardDb(boardRow);
812
- const ctx = { actor, board: boardRow, db };
813
-
814
- const ticket = resolveTicket(db, boardRow.id, options.ticket);
815
- const blockers = unresolvedBlockers(db, ticket.id);
816
- if (blockers.length && !options.force) {
817
- throw new Error(`Ticket #${ticket.number} is blocked: ${blockers.map((b) => `#${b.number} ${b.title}`).join(", ")}`);
818
- }
819
-
820
- const repoRoot = resolveRepoRoot(options, boardRow);
821
- const inProgress = stateByRole(db, boardRow.id, "in_progress") || stateByName(db, boardRow.id, "In Progress");
822
- if (!options.noSpawn && !options.dryRun && !inProgress) throw new Error("Board is missing an In Progress state.");
823
-
824
- if (options.dryRun) {
825
- return {
826
- dry_run: true,
827
- no_spawn: Boolean(options.noSpawn),
828
- run_id: null,
829
- board: { id: boardRow.id, slug: boardRow.slug, name: boardRow.name },
830
- ticket: { id: ticket.id, number: ticket.number, title: ticket.title },
831
- profile,
832
- policy: policyName || "none",
833
- repo_root: repoRoot,
834
- worktree_path: repoRoot,
835
- branch: options.branch || "",
836
- handoff_path: null,
837
- policy_bin: null,
838
- pid: null,
839
- spawned: false,
840
- prompt: "",
841
- run_record: "",
842
- run_record_path: null,
843
- verification_commands: verificationCommands
844
- };
845
- }
846
-
847
- const hermesExecutable = options.noSpawn ? null : preflightHermes(options, profile);
848
- const runId = `orbit-${ticket.number}-${profile}-${shortId()}`;
849
- const runDir = join(DISPATCH_RUNS_DIR, boardRow.slug, runId);
850
- const serverUrl = options.serverUrl || process.env.ORBIT_SERVER_URL || "";
851
- const displayServerUrl = sanitizeUrlBearingValue(serverUrl);
852
- const baseCommit = optionalCommandOutput("git", ["-C", repoRoot, "rev-parse", "HEAD"]);
853
- const environmentVariance = collectEnvironmentVariance();
854
- let branchName = options.branch || "";
855
- let worktreePath = "";
856
-
857
- if (options.worktree) {
858
- const worktree = ensureWorktree({
859
- repoRoot,
860
- boardSlug: boardRow.slug,
861
- ticketNumber: ticket.number,
862
- profile,
863
- branchName: options.branch,
864
- worktreePath: options.worktreePath
865
- });
866
- branchName = worktree.branchName;
867
- worktreePath = worktree.worktreePath;
868
- }
869
-
870
- mkdirSync(runDir, { recursive: true });
871
- const policyBin = createPolicyBin(runDir, policyName);
872
- const context = getContextPackFull(ticket.id, ctx, 1);
873
- const handoff = buildHandoff({
874
- boardRow,
875
- context,
876
- profile,
877
- repoRoot,
878
- worktreePath,
879
- branchName,
880
- baseCommit,
881
- policyName,
882
- runId,
883
- serverUrl: displayServerUrl,
884
- verificationCommands
885
- });
886
- const prompt = buildShortPrompt({ boardRow, ticket, profile, serverUrl: displayServerUrl });
887
- const handoffPath = join(runDir, "handoff.md");
888
- writeFileSync(handoffPath, handoff, "utf8");
889
-
890
- const hermesArgs = [];
891
- if (profile) hermesArgs.push("-p", profile);
892
- if (options.yolo) hermesArgs.push("--yolo");
893
- hermesArgs.push("chat", "-q", prompt);
894
- const runRecordPath = join(runDir, "run-record.json");
895
- const writeRunRecordFile = options.writeRunRecordFile || ((path, runRecordJson) => {
896
- writeFileSync(path, `${JSON.stringify(runRecordJson, null, 2)}\n`, "utf8");
897
- });
898
- const writeRunRecordJson = (status, childProcess = null) => {
899
- const runRecordJson = buildRunRecordJson({
900
- runId,
901
- status,
902
- boardRow,
903
- ticket,
904
- profile,
905
- policyName,
906
- repoRoot,
907
- worktreePath,
908
- branchName,
909
- baseCommit,
910
- handoffPath,
911
- policyBin,
912
- child: childProcess,
913
- hermesArgs,
914
- verificationCommands,
915
- options,
916
- serverUrl,
917
- environmentVariance
918
- });
919
- writeRunRecordFile(runRecordPath, runRecordJson);
920
- return runRecordJson;
921
- };
922
-
923
- let status = "prepared";
924
- writeRunRecordJson(status, null);
925
-
926
- touchBoardActive(boardRow.id);
927
- const planSummary = buildPlanExcerpt({ ticket, profile, handoffPath });
928
- if (options.noSpawn) {
929
- updateTicket(ticket.id, { ai_plan: planSummary, plan_artifact_path: handoffPath }, ctx);
930
- } else {
931
- updateTicket(ticket.id, { ai_plan: planSummary, plan_artifact_path: handoffPath, state_id: inProgress.id }, ctx);
932
- }
933
-
934
- const cwd = worktreePath || repoRoot;
935
- const env = {
936
- ...process.env,
937
- ORBIT_DISPATCH_RUN_ID: runId,
938
- ORBIT_BOARD: boardRow.slug,
939
- ORBIT_TICKET: String(ticket.number),
940
- ORBIT_TICKET_ID: ticket.id,
941
- ORBIT_HANDOFF_PATH: handoffPath,
942
- ORBIT_AUTONOMOUS_POLICY: policyName || "none",
943
- ...(policyBin ? { PATH: `${policyBin}:${process.env.PATH || ""}` } : {})
944
- };
945
-
946
- let child = null;
947
- if (!options.noSpawn) {
948
- child = spawn(hermesExecutable.command, [...hermesExecutable.argsPrefix, ...hermesArgs], {
949
- cwd,
950
- env,
951
- detached: !options.foreground,
952
- stdio: options.foreground ? "inherit" : "ignore"
953
- });
954
- if (!options.foreground) child.unref();
955
- status = "launched";
956
- writeRunRecordJson(status, child);
957
- }
958
-
959
- const runRecord = [
960
- `Orbit dispatch run ${options.noSpawn ? "prepared" : "started"}.`,
961
- `- run_id: ${runId}`,
962
- `- profile: ${profile}`,
963
- `- status: ${status}`,
964
- `- policy: ${policyName || "none"}`,
965
- `- ticket: #${ticket.number} ${ticket.title}`,
966
- `- branch: ${branchName || "current branch"}`,
967
- `- base_commit: ${baseCommit || "unknown"}`,
968
- `- worktree: ${worktreePath || repoRoot}`,
969
- `- handoff: linked markdown artifact${options.keepHandoffFile ? ` and ${handoffPath}` : ""}`,
970
- `- run_record_json: ${runRecordPath}`,
971
- `- policy_bin: ${policyBin || "none"}`,
972
- `- pid: ${child?.pid || "not spawned"}`,
973
- `- verification_commands: ${verificationCommands.join("; ")}`,
974
- `- command: ${options.hermesBin || "hermes"} ${hermesArgs.map((arg) => (arg.includes(" ") ? q(arg) : arg)).join(" ")}`,
975
- options.noSpawn ? "- mode: prepare-only; no agent spawned and ticket state left unchanged" : "- mode: spawned; ticket moved to In Progress"
976
- ].join("\n");
977
-
978
- createComment(ticket.id, { author: "orbit dispatch", kind: "agent_note", body: runRecord }, ctx);
979
-
980
- if (!options.keepHandoffFile) {
981
- // The canonical copy is the linked markdown artifact. Keep run metadata and
982
- // policy wrappers. Local-only agents may still use ORBIT_HANDOFF_PATH as a
983
- // bootstrap hint before they read the card, so leave the file in place.
984
- }
985
-
986
- return {
987
- dry_run: false,
988
- no_spawn: Boolean(options.noSpawn),
989
- run_id: runId,
990
- board: { id: boardRow.id, slug: boardRow.slug, name: boardRow.name },
991
- ticket: { id: ticket.id, number: ticket.number, title: ticket.title },
992
- profile,
993
- policy: policyName || "none",
994
- repo_root: repoRoot,
995
- worktree_path: worktreePath || repoRoot,
996
- branch: branchName || "",
997
- handoff_path: handoffPath,
998
- linked_handoff_path: handoffPath,
999
- policy_bin: policyBin,
1000
- pid: child?.pid || null,
1001
- spawned: Boolean(child),
1002
- prompt,
1003
- run_record: runRecord,
1004
- run_record_path: runRecordPath,
1005
- verification_commands: verificationCommands
1006
- };
1007
- }