@starlove/orbit 1.0.7 → 1.1.1
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/README.md +22 -7
- package/SKILL-ORBIT.md +26 -1
- package/dist/full/index.html +2 -2
- package/dist/full/js/app.a4f045fc.js +676 -0
- package/dist/full/js/app.a4f045fc.js.map +7 -0
- package/dist/full/{styles.14e9dbbd.css → styles.836872e7.css} +836 -39
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +23 -5
- package/docs/MCP_SETUP.md +5 -3
- package/docs/ORBIT_DISPATCH.md +185 -0
- package/docs/setup/orbit-init-wizard.md +174 -0
- package/package.json +2 -2
- package/public/js/actor-labels.js +65 -0
- package/public/js/app.js +8 -1
- package/public/js/card-actions.js +526 -0
- package/public/js/config.js +2 -1
- package/public/js/format.js +204 -10
- package/public/js/kanban.js +15 -0
- package/public/js/local-backend.js +24 -8
- package/public/js/router.js +3 -2
- package/public/js/search.js +6 -3
- package/public/js/settings.js +96 -26
- package/public/js/state.js +50 -0
- package/public/js/ticket-detail.js +428 -125
- package/public/styles.css +836 -39
- package/src/cli/orbit.js +217 -32
- package/src/core/agent.js +114 -24
- package/src/core/agents-md.js +15 -10
- package/src/core/attachments.js +238 -0
- package/src/core/auth.js +36 -0
- package/src/core/backups.js +9 -3
- package/src/core/board.js +32 -1
- package/src/core/boards.js +1 -1
- package/src/core/db.js +42 -2
- package/src/core/delete-board.js +87 -21
- package/src/core/dispatch.js +532 -0
- package/src/core/http.js +31 -11
- package/src/core/migrate.js +198 -0
- package/src/core/paths.js +6 -0
- package/src/core/registry.js +12 -9
- package/src/core/router.js +81 -7
- package/src/core/search.js +83 -10
- package/src/core/snapshots.js +14 -2
- package/src/mcp/orbit-client.js +109 -46
- package/src/mcp-server.js +238 -12
- package/src/server.js +20 -0
- package/dist/full/js/app.f4fd5e94.js +0 -604
- package/dist/full/js/app.f4fd5e94.js.map +0 -7
package/src/cli/orbit.js
CHANGED
|
@@ -1,36 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Orbit CLI — `orbit init` provisions `.orbit/board.db`, registry row, and
|
|
4
|
-
* copies SKILL-ORBIT.md into the target repo. Does not import board.js.
|
|
4
|
+
* copies managed SKILL-ORBIT.md into the target repo. Does not import board.js.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import {
|
|
9
|
-
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { removeOrbitAgentsSection, syncAgentsMd } from "../core/agents-md.js";
|
|
11
|
+
import { removeOrbitAgentsSection, syncAgentsMd, syncSkillOrbitMd } from "../core/agents-md.js";
|
|
12
12
|
|
|
13
13
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
14
14
|
|
|
15
15
|
function printUsage() {
|
|
16
16
|
console.log(`Usage:
|
|
17
|
+
orbit -v, --version Print the Orbit CLI version
|
|
17
18
|
orbit init [options] Provision .orbit/board.db + SKILL-ORBIT.md + AGENTS.md
|
|
18
19
|
orbit reset [options] Delete .orbit/ + SKILL-ORBIT.md + registry row
|
|
19
20
|
orbit serve [options] Start the Orbit web app + HTTP API
|
|
20
21
|
orbit docker [options] Build/run Orbit's web app in Docker
|
|
21
22
|
orbit mcp [options] Start the stdio MCP server (for agent clients)
|
|
23
|
+
orbit dispatch [options] Dispatch a Hermes profile on a ticket
|
|
22
24
|
|
|
23
|
-
Options (init / reset / serve / docker / mcp):
|
|
25
|
+
Options (init / reset / serve / docker / mcp / dispatch):
|
|
24
26
|
--cwd <dir> Project root (default: process.cwd())
|
|
25
27
|
|
|
26
28
|
Options (init only):
|
|
27
29
|
--no-ai Create the board with AI collaboration disabled
|
|
28
30
|
--example Create example onboarding tickets
|
|
29
|
-
--refresh-agents-md Overwrite SKILL-ORBIT.md and refresh Orbit section in AGENTS.md
|
|
30
31
|
|
|
31
32
|
Options (serve / docker):
|
|
32
33
|
--port <n> HTTP port (default: 3337, or $PORT)
|
|
33
34
|
|
|
35
|
+
Options (dispatch):
|
|
36
|
+
--board <slug-or-id> Local board to dispatch against (default: board for --cwd)
|
|
37
|
+
--ticket <number-id> Ticket number or id to dispatch
|
|
38
|
+
--profile <name> Hermes profile to run (default: agent)
|
|
39
|
+
--policy <name> Autonomous policy wrappers to apply (default: agent-safe; use none to disable)
|
|
40
|
+
--server-url <url> Refused for now; dispatch is local-board only
|
|
41
|
+
--remote Refused for now; use remote MCP/manual orchestration for hosted boards
|
|
42
|
+
--worktree Create and preserve a git worktree for review/testing
|
|
43
|
+
--worktree-path <dir> Worktree path when --worktree is used
|
|
44
|
+
--branch <name> Branch name for the preserved worktree
|
|
45
|
+
--hermes-bin <cmd> Hermes executable (default: hermes)
|
|
46
|
+
--no-spawn Prepare card/worktree/run record but do not start Hermes or move In Progress
|
|
47
|
+
--no-yolo Do not pass --yolo to Hermes (default dispatch passes --yolo)
|
|
48
|
+
--foreground Attach spawned Hermes process to this terminal
|
|
49
|
+
--keep-handoff-file Keep duplicate handoff file under DATA_DIR/dispatch-runs/<board>
|
|
50
|
+
--force Dispatch even when blockers exist
|
|
51
|
+
--dry-run Preview only; no files, worktrees, ticket fields, comments, or agents change
|
|
52
|
+
|
|
34
53
|
Options (docker only):
|
|
35
54
|
--image <name> Docker image tag (default: starscape-orbit:local)
|
|
36
55
|
--data-dir <dir> Docker registry/export data dir (default: <cwd>/.orbit/docker-data)
|
|
@@ -41,6 +60,11 @@ Options (docker only):
|
|
|
41
60
|
--dry-run Print docker commands without running them`);
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
function printVersion() {
|
|
64
|
+
const packageJson = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
65
|
+
console.log(packageJson.version);
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function shellQuote(value) {
|
|
45
69
|
return `"${String(value || "").replace(/"/g, '\\"')}"`;
|
|
46
70
|
}
|
|
@@ -59,7 +83,7 @@ async function loadCliCore() {
|
|
|
59
83
|
const [
|
|
60
84
|
{ disableAiCollaboration, enableAiCollaboration },
|
|
61
85
|
{ backupBoardDatabase, backupRegistry },
|
|
62
|
-
{ createRegistrySchema, openConnection },
|
|
86
|
+
{ closeConnection, createRegistrySchema, openConnection },
|
|
63
87
|
{ DATA_DIR, ROOT_DIR },
|
|
64
88
|
{ provisionRepoBoard },
|
|
65
89
|
{ deleteBoard, getBoardByRepoPath },
|
|
@@ -79,6 +103,7 @@ async function loadCliCore() {
|
|
|
79
103
|
ROOT_DIR,
|
|
80
104
|
backupBoardDatabase,
|
|
81
105
|
backupRegistry,
|
|
106
|
+
closeConnection,
|
|
82
107
|
createRegistrySchema,
|
|
83
108
|
deleteBoard,
|
|
84
109
|
disableAiCollaboration,
|
|
@@ -107,12 +132,36 @@ function disableAiIfRequested(options, registryRow, core) {
|
|
|
107
132
|
console.log("AI collaboration disabled for this board.");
|
|
108
133
|
}
|
|
109
134
|
|
|
135
|
+
function isBusyFilesystemError(error) {
|
|
136
|
+
const message = String(error?.message || "").toLowerCase();
|
|
137
|
+
return (
|
|
138
|
+
["EBUSY", "EPERM", "ENOTEMPTY"].includes(error?.code) ||
|
|
139
|
+
(error?.code === "ERR_SQLITE_ERROR" && (message.includes("locked") || message.includes("busy")))
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resetBusyError(error, projectRoot) {
|
|
144
|
+
if (!isBusyFilesystemError(error)) return error;
|
|
145
|
+
const message = [
|
|
146
|
+
`Orbit could not remove local board files under ${projectRoot}.`,
|
|
147
|
+
"",
|
|
148
|
+
"On Windows this usually means an Orbit web server, MCP helper, editor, or terminal still has .orbit/board.db open.",
|
|
149
|
+
"Close Orbit server terminals and restart any AI client with Orbit MCP enabled, then rerun:",
|
|
150
|
+
` orbit reset --cwd ${shellQuote(projectRoot)}`,
|
|
151
|
+
"",
|
|
152
|
+
`Original error: ${error.message}`
|
|
153
|
+
].join("\n");
|
|
154
|
+
const wrapped = new Error(message);
|
|
155
|
+
wrapped.code = error.code;
|
|
156
|
+
wrapped.cause = error;
|
|
157
|
+
return wrapped;
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
function parseArgs(argv) {
|
|
111
161
|
const args = {
|
|
112
162
|
command: null,
|
|
113
163
|
cwd: process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd(),
|
|
114
164
|
cwdProvided: false,
|
|
115
|
-
refreshAgentsMd: false,
|
|
116
165
|
ai: true,
|
|
117
166
|
example: false,
|
|
118
167
|
port: null,
|
|
@@ -121,18 +170,43 @@ function parseArgs(argv) {
|
|
|
121
170
|
containerName: null,
|
|
122
171
|
buildImage: true,
|
|
123
172
|
detach: true,
|
|
124
|
-
dryRun: false
|
|
173
|
+
dryRun: false,
|
|
174
|
+
board: null,
|
|
175
|
+
ticket: null,
|
|
176
|
+
profile: "agent",
|
|
177
|
+
policy: "agent-safe",
|
|
178
|
+
serverUrl: null,
|
|
179
|
+
worktree: false,
|
|
180
|
+
worktreePath: null,
|
|
181
|
+
branch: null,
|
|
182
|
+
hermesBin: "hermes",
|
|
183
|
+
noSpawn: false,
|
|
184
|
+
yolo: true,
|
|
185
|
+
keepHandoffFile: false,
|
|
186
|
+
force: false,
|
|
187
|
+
foreground: false,
|
|
188
|
+
remote: false,
|
|
189
|
+
help: false,
|
|
190
|
+
invalid: false
|
|
125
191
|
};
|
|
126
192
|
const rest = argv.slice(2);
|
|
127
193
|
if (rest.length === 0 || rest[0] === "-h" || rest[0] === "--help") {
|
|
128
194
|
args.command = "help";
|
|
129
195
|
return args;
|
|
130
196
|
}
|
|
197
|
+
if (rest[0] === "-v" || rest[0] === "--version") {
|
|
198
|
+
args.command = "version";
|
|
199
|
+
return args;
|
|
200
|
+
}
|
|
201
|
+
if (rest[0] === "dispatch" && (rest[1] === "-h" || rest[1] === "--help")) {
|
|
202
|
+
args.command = "dispatch";
|
|
203
|
+
args.help = true;
|
|
204
|
+
return args;
|
|
205
|
+
}
|
|
131
206
|
args.command = rest[0];
|
|
132
207
|
for (let i = 1; i < rest.length; i++) {
|
|
133
208
|
const a = rest[i];
|
|
134
|
-
if (a === "--
|
|
135
|
-
else if (a === "--no-ai") args.ai = false;
|
|
209
|
+
if (a === "--no-ai") args.ai = false;
|
|
136
210
|
else if (a === "--example") args.example = true;
|
|
137
211
|
else if (a === "--cwd" && rest[i + 1]) {
|
|
138
212
|
args.cwdProvided = true;
|
|
@@ -144,6 +218,52 @@ function parseArgs(argv) {
|
|
|
144
218
|
args.port = rest[++i];
|
|
145
219
|
} else if (a.startsWith("--port=")) {
|
|
146
220
|
args.port = a.slice("--port=".length);
|
|
221
|
+
} else if (a === "--board" && rest[i + 1]) {
|
|
222
|
+
args.board = rest[++i];
|
|
223
|
+
} else if (a.startsWith("--board=")) {
|
|
224
|
+
args.board = a.slice("--board=".length);
|
|
225
|
+
} else if (a === "--ticket" && rest[i + 1]) {
|
|
226
|
+
args.ticket = rest[++i];
|
|
227
|
+
} else if (a.startsWith("--ticket=")) {
|
|
228
|
+
args.ticket = a.slice("--ticket=".length);
|
|
229
|
+
} else if (a === "--profile" && rest[i + 1]) {
|
|
230
|
+
args.profile = rest[++i];
|
|
231
|
+
} else if (a.startsWith("--profile=")) {
|
|
232
|
+
args.profile = a.slice("--profile=".length);
|
|
233
|
+
} else if (a === "--policy" && rest[i + 1]) {
|
|
234
|
+
args.policy = rest[++i];
|
|
235
|
+
} else if (a.startsWith("--policy=")) {
|
|
236
|
+
args.policy = a.slice("--policy=".length);
|
|
237
|
+
} else if (a === "--server-url" && rest[i + 1]) {
|
|
238
|
+
args.serverUrl = rest[++i];
|
|
239
|
+
} else if (a.startsWith("--server-url=")) {
|
|
240
|
+
args.serverUrl = a.slice("--server-url=".length);
|
|
241
|
+
} else if (a === "--remote") {
|
|
242
|
+
args.remote = true;
|
|
243
|
+
} else if (a === "--worktree") {
|
|
244
|
+
args.worktree = true;
|
|
245
|
+
} else if (a === "--worktree-path" && rest[i + 1]) {
|
|
246
|
+
args.worktreePath = resolve(rest[++i]);
|
|
247
|
+
} else if (a.startsWith("--worktree-path=")) {
|
|
248
|
+
args.worktreePath = resolve(a.slice("--worktree-path=".length));
|
|
249
|
+
} else if (a === "--branch" && rest[i + 1]) {
|
|
250
|
+
args.branch = rest[++i];
|
|
251
|
+
} else if (a.startsWith("--branch=")) {
|
|
252
|
+
args.branch = a.slice("--branch=".length);
|
|
253
|
+
} else if (a === "--hermes-bin" && rest[i + 1]) {
|
|
254
|
+
args.hermesBin = rest[++i];
|
|
255
|
+
} else if (a.startsWith("--hermes-bin=")) {
|
|
256
|
+
args.hermesBin = a.slice("--hermes-bin=".length);
|
|
257
|
+
} else if (a === "--no-spawn") {
|
|
258
|
+
args.noSpawn = true;
|
|
259
|
+
} else if (a === "--no-yolo") {
|
|
260
|
+
args.yolo = false;
|
|
261
|
+
} else if (a === "--yolo") {
|
|
262
|
+
args.yolo = true;
|
|
263
|
+
} else if (a === "--keep-handoff-file") {
|
|
264
|
+
args.keepHandoffFile = true;
|
|
265
|
+
} else if (a === "--force") {
|
|
266
|
+
args.force = true;
|
|
147
267
|
} else if (a === "--image" && rest[i + 1]) {
|
|
148
268
|
args.image = rest[++i];
|
|
149
269
|
} else if (a.startsWith("--image=")) {
|
|
@@ -162,11 +282,13 @@ function parseArgs(argv) {
|
|
|
162
282
|
args.detach = true;
|
|
163
283
|
} else if (a === "--foreground") {
|
|
164
284
|
args.detach = false;
|
|
285
|
+
args.foreground = true;
|
|
165
286
|
} else if (a === "--dry-run") {
|
|
166
287
|
args.dryRun = true;
|
|
167
288
|
} else {
|
|
168
289
|
console.error(`Unknown argument: ${a}`);
|
|
169
290
|
args.command = "help";
|
|
291
|
+
args.invalid = true;
|
|
170
292
|
return args;
|
|
171
293
|
}
|
|
172
294
|
}
|
|
@@ -187,15 +309,13 @@ async function runInit(options) {
|
|
|
187
309
|
if (!existsSync(skillSrc)) {
|
|
188
310
|
console.warn(`Warning: missing ${skillSrc} — skipped copying SKILL-ORBIT.md and AGENTS.md Orbit section.`);
|
|
189
311
|
} else {
|
|
190
|
-
|
|
312
|
+
const skillSync = syncSkillOrbitMd(projectRoot, skillSrc);
|
|
313
|
+
if (skillSync.status === "same_file") {
|
|
191
314
|
console.log("SKILL-ORBIT.md source is already the project copy — left unchanged.");
|
|
192
|
-
} else if (existsSync(skillDest) && !options.refreshAgentsMd) {
|
|
193
|
-
console.log(`SKILL-ORBIT.md already exists — left unchanged (use --refresh-agents-md to replace).`);
|
|
194
315
|
} else {
|
|
195
|
-
copyFileSync(skillSrc, skillDest);
|
|
196
316
|
console.log(`Wrote ${skillDest}`);
|
|
197
317
|
}
|
|
198
|
-
syncAgentsMd(projectRoot
|
|
318
|
+
syncAgentsMd(projectRoot);
|
|
199
319
|
}
|
|
200
320
|
|
|
201
321
|
enableAiIfEnabled(options, result.registryRow, core);
|
|
@@ -204,7 +324,7 @@ async function runInit(options) {
|
|
|
204
324
|
if (result.message && result.status === "skipped") {
|
|
205
325
|
console.log(result.message);
|
|
206
326
|
} else if (result.status === "created") {
|
|
207
|
-
console.log(`Initialized Orbit board at ${
|
|
327
|
+
console.log(`Initialized Orbit board at ${result.registryRow?.db_path ?? "(unknown path)"}`);
|
|
208
328
|
} else if (result.status === "repaired") {
|
|
209
329
|
console.log(result.message || "Linked existing board database to the registry.");
|
|
210
330
|
}
|
|
@@ -231,6 +351,7 @@ async function runReset(options) {
|
|
|
231
351
|
DATA_DIR,
|
|
232
352
|
backupBoardDatabase,
|
|
233
353
|
backupRegistry,
|
|
354
|
+
closeConnection,
|
|
234
355
|
createRegistrySchema,
|
|
235
356
|
deleteBoard,
|
|
236
357
|
getBoardByRepoPath,
|
|
@@ -247,23 +368,53 @@ async function runReset(options) {
|
|
|
247
368
|
const registryRow = getBoardByRepoPath(projectRoot);
|
|
248
369
|
|
|
249
370
|
if (registryRow && existsSync(registryRow.db_path)) {
|
|
250
|
-
|
|
371
|
+
const db = openConnection(registryRow.db_path);
|
|
372
|
+
try {
|
|
373
|
+
backupBoardDatabase(registryRow, db, "pre-cli-reset");
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw resetBusyError(error, projectRoot);
|
|
376
|
+
} finally {
|
|
377
|
+
closeConnection(registryRow.db_path);
|
|
378
|
+
}
|
|
251
379
|
}
|
|
252
380
|
if (registryRow) backupRegistry("pre-cli-reset");
|
|
253
381
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
382
|
+
try {
|
|
383
|
+
// Remove the board database. With central storage the db lives outside the
|
|
384
|
+
// project directory; with legacy in-repo storage it is inside orbitDir below.
|
|
385
|
+
const isCentralDb = registryRow && !registryRow.db_path.startsWith(projectRoot + "/");
|
|
386
|
+
if (isCentralDb && existsSync(registryRow.db_path)) {
|
|
387
|
+
const boardDir = dirname(registryRow.db_path);
|
|
388
|
+
rmSync(boardDir, { recursive: true, force: true });
|
|
389
|
+
removed.push(boardDir);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Remove central dispatch-runs for this board.
|
|
393
|
+
if (registryRow) {
|
|
394
|
+
const dispatchRunsDir = join(DATA_DIR, "dispatch-runs", registryRow.slug);
|
|
395
|
+
if (existsSync(dispatchRunsDir)) {
|
|
396
|
+
rmSync(dispatchRunsDir, { recursive: true, force: true });
|
|
397
|
+
removed.push(dispatchRunsDir);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Remove the in-repo .orbit/ directory (covers legacy db, residual dispatch-runs, etc.).
|
|
402
|
+
if (existsSync(orbitDir)) {
|
|
403
|
+
rmSync(orbitDir, { recursive: true, force: true });
|
|
404
|
+
removed.push(orbitDir);
|
|
405
|
+
}
|
|
406
|
+
if (existsSync(skillMd)) {
|
|
407
|
+
unlinkSync(skillMd);
|
|
408
|
+
removed.push(skillMd);
|
|
409
|
+
}
|
|
410
|
+
const agentsCleanup = removeOrbitAgentsSection(projectRoot);
|
|
411
|
+
if (agentsCleanup.removed) {
|
|
412
|
+
removed.push(`${agentsCleanup.path} Orbit section`);
|
|
413
|
+
} else if (!agentsCleanup.ok) {
|
|
414
|
+
console.warn(`Warning: skipped AGENTS.md cleanup (${agentsCleanup.reason}) in ${agentsCleanup.path}`);
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
throw resetBusyError(error, projectRoot);
|
|
267
418
|
}
|
|
268
419
|
|
|
269
420
|
if (registryRow) {
|
|
@@ -284,6 +435,7 @@ async function runServe(options) {
|
|
|
284
435
|
// captures it at module load. cwd defaults are equivalent for most users, but
|
|
285
436
|
// an explicit --cwd lets a global install target a board outside cwd.
|
|
286
437
|
process.env.PROJECT_ROOT = resolve(options.cwd);
|
|
438
|
+
process.env.ORBIT_SYNC_MANAGED_SKILL_ORBIT = "1";
|
|
287
439
|
if (options.port) process.env.PORT = String(options.port);
|
|
288
440
|
await import("../server.js");
|
|
289
441
|
}
|
|
@@ -385,6 +537,35 @@ async function runMcp(options) {
|
|
|
385
537
|
await import("../mcp-server.js");
|
|
386
538
|
}
|
|
387
539
|
|
|
540
|
+
async function runDispatch(options) {
|
|
541
|
+
if (options.help) {
|
|
542
|
+
printUsage();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
process.env.PROJECT_ROOT = resolve(options.cwd);
|
|
546
|
+
const { dispatchTicket } = await import("../core/dispatch.js");
|
|
547
|
+
const result = dispatchTicket(options);
|
|
548
|
+
if (result.dry_run) {
|
|
549
|
+
console.log(`Dry run: would dispatch ticket #${result.ticket.number} ${result.ticket.title}`);
|
|
550
|
+
console.log(`Board: ${result.board.slug}`);
|
|
551
|
+
console.log(`Profile: ${result.profile}`);
|
|
552
|
+
console.log(`Mode: ${result.no_spawn ? "prepare-only" : "spawn"}`);
|
|
553
|
+
console.log("No files, worktrees, ticket fields, comments, or agents were changed.");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
console.log(`Dispatch ${result.spawned ? "started" : "prepared"}: ${result.run_id}`);
|
|
557
|
+
console.log(`Board: ${result.board.slug}`);
|
|
558
|
+
console.log(`Ticket: #${result.ticket.number} ${result.ticket.title}`);
|
|
559
|
+
console.log(`Profile: ${result.profile}`);
|
|
560
|
+
console.log(`Policy: ${result.policy}`);
|
|
561
|
+
console.log(`Worktree: ${result.worktree_path}`);
|
|
562
|
+
if (result.branch) console.log(`Branch: ${result.branch}`);
|
|
563
|
+
if (result.pid) console.log(`PID: ${result.pid}`);
|
|
564
|
+
console.log("AI Written-Plan updated with the generated handoff.");
|
|
565
|
+
console.log("Run record comment added to the ticket.");
|
|
566
|
+
if (result.no_spawn) console.log("No agent spawned; ticket state left unchanged.");
|
|
567
|
+
}
|
|
568
|
+
|
|
388
569
|
async function main() {
|
|
389
570
|
const args = parseArgs(process.argv);
|
|
390
571
|
try {
|
|
@@ -398,9 +579,13 @@ async function main() {
|
|
|
398
579
|
await runDocker(args);
|
|
399
580
|
} else if (args.command === "mcp") {
|
|
400
581
|
await runMcp(args);
|
|
582
|
+
} else if (args.command === "dispatch") {
|
|
583
|
+
await runDispatch(args);
|
|
584
|
+
} else if (args.command === "version") {
|
|
585
|
+
printVersion();
|
|
401
586
|
} else {
|
|
402
587
|
printUsage();
|
|
403
|
-
process.exitCode = args.command === "help" ? 0 : 1;
|
|
588
|
+
process.exitCode = args.command === "help" && !args.invalid ? 0 : 1;
|
|
404
589
|
}
|
|
405
590
|
} catch (err) {
|
|
406
591
|
console.error(err?.message || err);
|
package/src/core/agent.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
relationRows,
|
|
8
8
|
stateByName,
|
|
9
9
|
stateByRole,
|
|
10
|
+
stateById,
|
|
10
11
|
ticketById,
|
|
11
12
|
ticketByNumber,
|
|
12
13
|
ticketByTitle,
|
|
@@ -112,9 +113,6 @@ export function readTicket(lookup, ctx) {
|
|
|
112
113
|
const innerBoard = boardById(db, ticket.board_id);
|
|
113
114
|
requireBoardAccess(actor, innerBoard);
|
|
114
115
|
|
|
115
|
-
const comments = db
|
|
116
|
-
.prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
|
|
117
|
-
.all(ticket.id);
|
|
118
116
|
const labels = labelsForTicket(db, ticket.id);
|
|
119
117
|
|
|
120
118
|
return {
|
|
@@ -128,7 +126,6 @@ export function readTicket(lookup, ctx) {
|
|
|
128
126
|
priority: ticket.priority,
|
|
129
127
|
labels
|
|
130
128
|
},
|
|
131
|
-
comments,
|
|
132
129
|
board_manual: boardManual(innerBoard.id, ctx)
|
|
133
130
|
};
|
|
134
131
|
}
|
|
@@ -213,14 +210,16 @@ export function completeTicket(body, ctx) {
|
|
|
213
210
|
return getContextPack(ticket.id, ctx, 1);
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
export function getContextPack(ticketId, ctx, depth = 1) {
|
|
213
|
+
export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
|
|
217
214
|
const { db, board, actor } = ctx;
|
|
215
|
+
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
|
|
216
|
+
const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
|
|
217
|
+
const includeRelatedFull = Boolean(options.include_related_full ?? options.includeRelatedFull);
|
|
218
218
|
const ticket = ticketById(db, ticketId);
|
|
219
219
|
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
220
220
|
const innerBoard = boardById(db, ticket.board_id);
|
|
221
221
|
requireBoardAccess(actor, innerBoard);
|
|
222
222
|
|
|
223
|
-
const comments = db.prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC").all(ticketId);
|
|
224
223
|
const labels = labelsForTicket(db, ticketId);
|
|
225
224
|
const relations = relationRows(db, ticketId);
|
|
226
225
|
const blockers = unresolvedBlockers(db, ticketId);
|
|
@@ -236,33 +235,124 @@ export function getContextPack(ticketId, ctx, depth = 1) {
|
|
|
236
235
|
...childTickets,
|
|
237
236
|
...(parentTicket ? [parentTicket] : [])
|
|
238
237
|
]);
|
|
239
|
-
const relatedCommentRows =
|
|
240
|
-
depth > 0
|
|
241
|
-
? relatedTickets.flatMap((related) =>
|
|
242
|
-
db
|
|
243
|
-
.prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT 5")
|
|
244
|
-
.all(related.id)
|
|
245
|
-
.reverse()
|
|
246
|
-
.map((comment) => ({ ...comment, ticket_id: related.id, ticket_title: related.title }))
|
|
247
|
-
)
|
|
248
|
-
: [];
|
|
249
238
|
|
|
250
239
|
return {
|
|
251
|
-
ticket: {
|
|
240
|
+
ticket: capTicketFields({
|
|
252
241
|
...ticket,
|
|
253
242
|
labels
|
|
254
|
-
},
|
|
243
|
+
}, maxCharsPerField),
|
|
255
244
|
board: innerBoard,
|
|
256
245
|
board_manual: boardManual(innerBoard.id, ctx),
|
|
257
|
-
|
|
258
|
-
parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
|
|
246
|
+
parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
|
|
259
247
|
child_tickets: childTickets.map((child) => ({
|
|
260
|
-
...child,
|
|
248
|
+
...capTicketFields(child, maxCharsPerField),
|
|
261
249
|
child_count: childTicketsFor(db, child.id).length
|
|
262
250
|
})),
|
|
263
|
-
relations
|
|
251
|
+
relations: relations.map((relation) => ({
|
|
252
|
+
...relation,
|
|
253
|
+
other_ticket: includeRelatedFull
|
|
254
|
+
? capTicketFields({ ...relation.other_ticket, labels: labelsForTicket(db, relation.other_ticket.id) }, maxCharsPerField)
|
|
255
|
+
: compactTicket(relation.other_ticket)
|
|
256
|
+
})),
|
|
264
257
|
blockers,
|
|
265
|
-
related_tickets: relatedTickets
|
|
266
|
-
|
|
258
|
+
related_tickets: relatedTickets.map((related) =>
|
|
259
|
+
includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
|
|
260
|
+
)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
|
|
265
|
+
const { db, board, actor } = ctx;
|
|
266
|
+
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 4000);
|
|
267
|
+
const commentLimit = Math.min(nonNegativeInt(options.comment_limit ?? options.commentLimit, 5), 20);
|
|
268
|
+
const ticket = ticketById(db, ticketId);
|
|
269
|
+
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
270
|
+
const innerBoard = boardById(db, ticket.board_id);
|
|
271
|
+
requireBoardAccess(actor, innerBoard);
|
|
272
|
+
|
|
273
|
+
const labels = labelsForTicket(db, ticket.id).map((label) => label.name);
|
|
274
|
+
const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
|
|
275
|
+
const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
|
|
276
|
+
const comments = db
|
|
277
|
+
.prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
278
|
+
.all(ticket.id, commentLimit)
|
|
279
|
+
.reverse()
|
|
280
|
+
.map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
board: {
|
|
284
|
+
id: innerBoard.id,
|
|
285
|
+
slug: innerBoard.slug,
|
|
286
|
+
name: innerBoard.name,
|
|
287
|
+
repo_path: innerBoard.repo_path || innerBoard.system_path || "",
|
|
288
|
+
system_path: innerBoard.system_path || innerBoard.repo_path || "",
|
|
289
|
+
default_branch: innerBoard.default_branch || "main",
|
|
290
|
+
agent_instructions: capString(innerBoard.agent_instructions || "", maxCharsPerField)
|
|
291
|
+
},
|
|
292
|
+
ticket: {
|
|
293
|
+
id: ticket.id,
|
|
294
|
+
ticket_id: ticket.id,
|
|
295
|
+
number: ticket.number,
|
|
296
|
+
title: ticket.title,
|
|
297
|
+
type: ticket.type,
|
|
298
|
+
priority: ticket.priority,
|
|
299
|
+
state_id: ticket.state_id,
|
|
300
|
+
state_name: ticket.state_name,
|
|
301
|
+
state_role: ticket.state_role,
|
|
302
|
+
description: capString(ticket.description || "", maxCharsPerField),
|
|
303
|
+
acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
|
|
304
|
+
ai_plan: capString(ticket.ai_plan || "", maxCharsPerField),
|
|
305
|
+
labels
|
|
306
|
+
},
|
|
307
|
+
blockers: {
|
|
308
|
+
can_start: blockers.length === 0,
|
|
309
|
+
blockers
|
|
310
|
+
},
|
|
311
|
+
parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
|
|
312
|
+
relevant_state_ids: relevantStateIds(db, innerBoard.id, ticket.state_id),
|
|
313
|
+
recent_comments: comments
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function positiveInt(value, fallback) {
|
|
318
|
+
const n = Number(value);
|
|
319
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function nonNegativeInt(value, fallback) {
|
|
323
|
+
const n = Number(value);
|
|
324
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function capString(value, maxChars) {
|
|
328
|
+
const text = String(value || "");
|
|
329
|
+
return maxChars && text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function capTicketFields(ticket, maxChars) {
|
|
333
|
+
if (!maxChars) return ticket;
|
|
334
|
+
const copy = { ...ticket };
|
|
335
|
+
for (const field of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
|
|
336
|
+
if (Object.hasOwn(copy, field)) copy[field] = capString(copy[field], maxChars);
|
|
337
|
+
}
|
|
338
|
+
return copy;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function extractAcceptance(description) {
|
|
342
|
+
const match = String(description || "").match(/(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i);
|
|
343
|
+
return match ? match[1].trim() : "";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function relevantStateIds(db, boardId, currentStateId) {
|
|
347
|
+
const byRole = (role) => stateByRole(db, boardId, role)?.id || null;
|
|
348
|
+
const byName = (name) => stateByName(db, boardId, name)?.id || null;
|
|
349
|
+
return {
|
|
350
|
+
current: currentStateId,
|
|
351
|
+
current_role: stateById(db, currentStateId)?.role || null,
|
|
352
|
+
ai_ready: byRole("ai_ready") || byName("AI Ready"),
|
|
353
|
+
in_progress: byRole("in_progress") || byName("In Progress"),
|
|
354
|
+
review: byRole("review") || byName("Review"),
|
|
355
|
+
done: byRole("done") || byName("Done"),
|
|
356
|
+
todo: byRole("todo") || byName("Todo")
|
|
267
357
|
};
|
|
268
358
|
}
|
package/src/core/agents-md.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
export const ORBIT_AGENTS_START = "<!-- ORBIT:AGENTS-START -->";
|
|
@@ -21,7 +21,19 @@ export function buildAgentsMd() {
|
|
|
21
21
|
${orbitAgentsSection()}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function
|
|
24
|
+
export function syncSkillOrbitMd(projectRoot, skillSrc) {
|
|
25
|
+
const skillDest = resolve(projectRoot, "SKILL-ORBIT.md");
|
|
26
|
+
if (!existsSync(skillSrc)) {
|
|
27
|
+
return { ok: false, status: "missing_source", source: skillSrc, path: skillDest };
|
|
28
|
+
}
|
|
29
|
+
if (resolve(skillSrc) === skillDest) {
|
|
30
|
+
return { ok: true, status: "same_file", source: skillSrc, path: skillDest };
|
|
31
|
+
}
|
|
32
|
+
copyFileSync(skillSrc, skillDest);
|
|
33
|
+
return { ok: true, status: "written", source: skillSrc, path: skillDest };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function syncAgentsMd(projectRoot, _options = {}, logger = console.log) {
|
|
25
37
|
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
26
38
|
const section = orbitAgentsSection();
|
|
27
39
|
|
|
@@ -36,14 +48,7 @@ export function syncAgentsMd(projectRoot, options = {}, logger = console.log) {
|
|
|
36
48
|
const endIndex = current.indexOf(ORBIT_AGENTS_END);
|
|
37
49
|
|
|
38
50
|
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
39
|
-
|
|
40
|
-
logger("AGENTS.md already includes Orbit instructions — left unchanged (use --refresh-agents-md to replace).");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const before = current.slice(0, startIndex).replace(/\s*$/, "\n\n");
|
|
44
|
-
const after = current.slice(endIndex + ORBIT_AGENTS_END.length).replace(/^\s*/, "");
|
|
45
|
-
writeFileSync(agentsPath, `${before}${section}${after}`, "utf8");
|
|
46
|
-
logger(`Refreshed Orbit instructions inside ${agentsPath}`);
|
|
51
|
+
logger("AGENTS.md already includes Orbit instructions — left unchanged.");
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
49
54
|
|