@starlove/orbit 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
package/src/core/dispatch.js
DELETED
|
@@ -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
|
-
}
|