@starlove/orbit 1.3.5 → 1.4.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/Dockerfile +2 -1
- package/README.md +10 -9
- package/SKILL-ORBIT.md +41 -17
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
- package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/OFL.txt +93 -0
- package/dist/full/fonts/Inter/README.txt +118 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
- package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
- package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/README.txt +100 -0
- package/dist/full/fonts/Orbitron/OFL.txt +93 -0
- package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/Orbitron/README.txt +68 -0
- package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/dist/full/fonts/runescape-uf.ttf +0 -0
- package/dist/full/index.html +135 -25
- package/dist/full/js/app.4e6fbe20.js +952 -0
- package/dist/full/js/app.4e6fbe20.js.map +7 -0
- package/dist/full/styles.4e60d824.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +20 -13
- package/docs/ORBIT_DISPATCH.md +73 -38
- package/docs/UPDATING.md +114 -0
- package/package.json +5 -7
- 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 +93 -0
- 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 +93 -0
- package/public/fonts/Inter/README.txt +118 -0
- 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/OFL.txt +93 -0
- package/public/fonts/JetBrains_Mono/README.txt +79 -0
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OFL.txt +93 -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 +100 -0
- package/public/fonts/Orbitron/OFL.txt +93 -0
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +68 -0
- package/public/fonts/Rajdhani/OFL.txt +93 -0
- 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 +133 -23
- package/public/js/artifact-drawer.js +79 -0
- package/public/js/board-export.js +161 -0
- package/public/js/board-menu.js +219 -17
- package/public/js/card-actions.js +1 -1
- package/public/js/create-card.js +60 -23
- package/public/js/drawer.js +164 -4
- package/public/js/font-preference.js +230 -0
- package/public/js/format.js +0 -10
- package/public/js/icons.js +16 -0
- package/public/js/local-backend.js +31 -4
- package/public/js/main.js +7 -1
- package/public/js/modal.js +3 -1
- package/public/js/panel.js +58 -0
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +606 -275
- package/public/js/sse.js +3 -0
- package/public/js/state.js +1 -1
- package/public/js/text-editing.js +128 -0
- package/public/js/ticket-detail.js +377 -148
- package/public/js/ticket-typeahead.js +49 -0
- package/public/js/toast.js +19 -4
- package/public/js/topbar-notes.js +3 -3
- package/public/js/typeahead.js +206 -0
- package/public/js/update-check.js +58 -0
- package/public/styles.css +5447 -3572
- package/src/cli/orbit.js +9 -5
- package/src/core/agent.js +12 -4
- package/src/core/agents-md.js +3 -3
- package/src/core/artifacts.js +149 -0
- package/src/core/board.js +3 -2
- package/src/core/boards.js +0 -1
- package/src/core/css-bundle.js +1 -0
- package/src/core/db.js +6 -0
- package/src/core/dispatch.js +344 -34
- package/src/core/http.js +9 -2
- package/src/core/paths.js +26 -2
- package/src/core/router.js +33 -0
- package/src/core/search.js +3 -2
- package/src/core/snapshots.js +10 -5
- package/src/core/tickets.js +34 -3
- package/src/core/util.js +3 -2
- package/src/core/version-check.js +154 -0
- package/src/mcp/orbit-client.js +18 -2
- package/src/mcp-server.js +19 -14
- package/src/server.js +28 -3
- package/dist/full/js/app.4ae80764.js +0 -742
- package/dist/full/js/app.4ae80764.js.map +0 -7
- package/dist/full/styles.b9e0c130.css +0 -1
package/src/cli/orbit.js
CHANGED
|
@@ -41,8 +41,8 @@ Options (dispatch):
|
|
|
41
41
|
--ticket <number-id> Ticket number or id to dispatch
|
|
42
42
|
--profile <name> Hermes profile to run (default: agent)
|
|
43
43
|
--policy <name> Autonomous policy wrappers to apply (default: agent-safe; use none to disable)
|
|
44
|
-
--server-url <url>
|
|
45
|
-
--remote
|
|
44
|
+
--server-url <url> Hosted Orbit base URL for remote dispatch
|
|
45
|
+
--remote Force hosted/remote dispatch mode
|
|
46
46
|
--worktree Create and preserve a git worktree for review/testing
|
|
47
47
|
--worktree-path <dir> Worktree path when --worktree is used
|
|
48
48
|
--branch <name> Branch name for the preserved worktree
|
|
@@ -186,6 +186,7 @@ function parseArgs(argv) {
|
|
|
186
186
|
dryRun: false,
|
|
187
187
|
board: null,
|
|
188
188
|
ticket: null,
|
|
189
|
+
ticketUrl: null,
|
|
189
190
|
profile: "agent",
|
|
190
191
|
policy: "agent-safe",
|
|
191
192
|
serverUrl: null,
|
|
@@ -312,6 +313,9 @@ function parseArgs(argv) {
|
|
|
312
313
|
args.foreground = true;
|
|
313
314
|
} else if (a === "--dry-run") {
|
|
314
315
|
args.dryRun = true;
|
|
316
|
+
} else if (args.command === "dispatch" && /^https?:\/\//i.test(a) && !args.ticketUrl) {
|
|
317
|
+
args.ticketUrl = a;
|
|
318
|
+
args.remote = true;
|
|
315
319
|
} else {
|
|
316
320
|
console.error(`Unknown argument: ${a}`);
|
|
317
321
|
args.command = "help";
|
|
@@ -601,8 +605,8 @@ async function runDispatch(options) {
|
|
|
601
605
|
return;
|
|
602
606
|
}
|
|
603
607
|
process.env.PROJECT_ROOT = resolve(options.cwd);
|
|
604
|
-
const {
|
|
605
|
-
const result =
|
|
608
|
+
const { dispatchTicketAsync } = await import("../core/dispatch.js");
|
|
609
|
+
const result = await dispatchTicketAsync(options);
|
|
606
610
|
if (result.dry_run) {
|
|
607
611
|
console.log(`Dry run: would dispatch ticket #${result.ticket.number} ${result.ticket.title}`);
|
|
608
612
|
console.log(`Board: ${result.board.slug}`);
|
|
@@ -620,7 +624,7 @@ async function runDispatch(options) {
|
|
|
620
624
|
if (result.branch) console.log(`Branch: ${result.branch}`);
|
|
621
625
|
if (result.run_record_path) console.log(`Run record: ${result.run_record_path}`);
|
|
622
626
|
if (result.pid) console.log(`PID: ${result.pid}`);
|
|
623
|
-
console.log("AI
|
|
627
|
+
console.log("AI Plan updated with a terse summary and linked handoff artifact.");
|
|
624
628
|
console.log("Run record comment added to the ticket.");
|
|
625
629
|
if (result.no_spawn) console.log("No agent spawned; ticket state left unchanged.");
|
|
626
630
|
}
|
package/src/core/agent.js
CHANGED
|
@@ -190,12 +190,16 @@ export function completeTicket(body, ctx) {
|
|
|
190
190
|
const lines = [summary];
|
|
191
191
|
if (body.pr_url) lines.push(`PR: ${body.pr_url}`);
|
|
192
192
|
const updates = body.updates ? appendFieldNote(ticket.implementation_updates, body.updates, actor.name) : ticket.implementation_updates;
|
|
193
|
+
const implementationArtifactPath = body.implementation_artifact_path || body.summary_artifact_path || ticket.implementation_artifact_path || "";
|
|
193
194
|
tx(db, () => {
|
|
194
195
|
const time = now();
|
|
195
196
|
addComment(db, ticket.id, actor.name, "completion", lines.join("\n\n"));
|
|
196
197
|
db.prepare(
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
`UPDATE tickets
|
|
199
|
+
SET state_id = ?, implementation_summary = ?,
|
|
200
|
+
implementation_artifact_path = ?, implementation_updates = ?, updated_at = ?
|
|
201
|
+
WHERE id = ?`
|
|
202
|
+
).run(nextState.id, summary, implementationArtifactPath, updates, time, ticket.id);
|
|
199
203
|
recordEvent(db, ticket.board_id, "agent_completed", ticket.id, actor.name, {
|
|
200
204
|
from: ticket.state_name,
|
|
201
205
|
next_state: nextState.name,
|
|
@@ -252,7 +256,7 @@ function buildContextPack(ticketId, ctx, depth = 1, options = {}, { includeProje
|
|
|
252
256
|
...ticket,
|
|
253
257
|
labels
|
|
254
258
|
}, maxCharsPerField),
|
|
255
|
-
board:
|
|
259
|
+
board: contextBoardIdentity(innerBoard, ctx),
|
|
256
260
|
parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
|
|
257
261
|
child_tickets: childTickets.map((child) => ({
|
|
258
262
|
...capTicketFields(child, maxCharsPerField),
|
|
@@ -295,7 +299,7 @@ export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
|
|
|
295
299
|
const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
|
|
296
300
|
const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
|
|
297
301
|
const comments = db
|
|
298
|
-
.prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
302
|
+
.prepare("SELECT id, author, kind, body, created_at FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at DESC LIMIT ?")
|
|
299
303
|
.all(ticket.id, commentLimit)
|
|
300
304
|
.reverse()
|
|
301
305
|
.map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
|
|
@@ -323,6 +327,10 @@ export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
|
|
|
323
327
|
description: capString(ticket.description || "", maxCharsPerField),
|
|
324
328
|
acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
|
|
325
329
|
ai_plan: capString(ticket.ai_plan || "", maxCharsPerField),
|
|
330
|
+
plan_artifact_path: ticket.plan_artifact_path || "",
|
|
331
|
+
implementation_summary: capString(ticket.implementation_summary || "", maxCharsPerField),
|
|
332
|
+
implementation_artifact_path: ticket.implementation_artifact_path || "",
|
|
333
|
+
implementation_updates: capString(ticket.implementation_updates || "", maxCharsPerField),
|
|
326
334
|
labels
|
|
327
335
|
},
|
|
328
336
|
blockers: {
|
package/src/core/agents-md.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
export const ORBIT_AGENTS_START = "<!--
|
|
5
|
-
export const ORBIT_AGENTS_END = "
|
|
4
|
+
export const ORBIT_AGENTS_START = "<!--ORBIT-->";
|
|
5
|
+
export const ORBIT_AGENTS_END = "<!--/ORBIT-->";
|
|
6
6
|
export const ORBIT_AGENTS_CANONICAL_LINE =
|
|
7
7
|
"Git is canonical for code. `SKILL-ORBIT.md` is canonical for Orbit/kanban/ticket/card workflow.";
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ export function orbitAgentsSection() {
|
|
|
10
10
|
return `${ORBIT_AGENTS_START}
|
|
11
11
|
${ORBIT_AGENTS_CANONICAL_LINE}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
For Orbit, kanban, board, lane, ticket, card, epic, blocker, claim, AI Ready, implementation fields, planning state, project memory, or handoff work: read \`SKILL-ORBIT.md\` first.
|
|
14
14
|
Use Orbit API/MCP tools for tickets/cards; do not edit .orbit/board.db directly.
|
|
15
15
|
${ORBIT_AGENTS_END}\n`;
|
|
16
16
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative, sep } from "node:path";
|
|
3
|
+
import { boardById, ticketById } from "./queries.js";
|
|
4
|
+
import { requireBoardAccess } from "./auth.js";
|
|
5
|
+
import { DISPATCH_RUNS_DIR } from "./paths.js";
|
|
6
|
+
import { httpError } from "./util.js";
|
|
7
|
+
|
|
8
|
+
const MAX_MARKDOWN_ARTIFACT_BYTES = 512 * 1024;
|
|
9
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
|
|
10
|
+
|
|
11
|
+
function cleanPath(value) {
|
|
12
|
+
const text = String(value || "").trim();
|
|
13
|
+
if (!text || text.includes("\0")) throw httpError(400, "artifact_path_required");
|
|
14
|
+
return text.replace(/\\/g, "/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureInside(path, root) {
|
|
18
|
+
if (!root) return false;
|
|
19
|
+
const normalizedRoot = normalize(root + sep);
|
|
20
|
+
const normalizedPath = normalize(path);
|
|
21
|
+
return normalizedPath === normalize(root) || normalizedPath.startsWith(normalizedRoot);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pathAliases(path) {
|
|
25
|
+
const aliases = new Set([path]);
|
|
26
|
+
const mntDrive = path.match(/^\/mnt\/([a-z])\/(.+)$/i);
|
|
27
|
+
if (mntDrive) {
|
|
28
|
+
const drive = mntDrive[1].toLowerCase();
|
|
29
|
+
aliases.add(`/${drive}/${mntDrive[2]}`);
|
|
30
|
+
aliases.add(`${drive.toUpperCase()}:/${mntDrive[2]}`);
|
|
31
|
+
}
|
|
32
|
+
const slashDrive = path.match(/^\/([a-z])\/(.+)$/i);
|
|
33
|
+
if (slashDrive) {
|
|
34
|
+
const drive = slashDrive[1].toLowerCase();
|
|
35
|
+
aliases.add(`/mnt/${drive}/${slashDrive[2]}`);
|
|
36
|
+
aliases.add(`${drive.toUpperCase()}:/${slashDrive[2]}`);
|
|
37
|
+
}
|
|
38
|
+
const winDrive = path.match(/^([a-z]):\/(.+)$/i);
|
|
39
|
+
if (winDrive) {
|
|
40
|
+
const drive = winDrive[1].toLowerCase();
|
|
41
|
+
aliases.add(`/mnt/${drive}/${winDrive[2]}`);
|
|
42
|
+
aliases.add(`/${drive}/${winDrive[2]}`);
|
|
43
|
+
}
|
|
44
|
+
return [...aliases];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isAbsoluteArtifactPath(path) {
|
|
48
|
+
return isAbsolute(path) || /^[a-z]:\//i.test(path);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function artifactRoots(ctx) {
|
|
52
|
+
const roots = [];
|
|
53
|
+
const repoRoot = ctx.board.repo_path || ctx.board.system_path || ctx.board.project_root || "";
|
|
54
|
+
if (repoRoot) roots.push(normalize(repoRoot));
|
|
55
|
+
roots.push(normalize(DISPATCH_RUNS_DIR));
|
|
56
|
+
if (ctx.board.db_path) {
|
|
57
|
+
roots.push(normalize(join(dirname(ctx.board.db_path), "artifacts")));
|
|
58
|
+
roots.push(normalize(join(dirname(ctx.board.db_path), "dispatch-runs")));
|
|
59
|
+
}
|
|
60
|
+
return [...new Set(roots.filter(Boolean))];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveArtifactPath(ctx, requestedPath) {
|
|
64
|
+
const raw = cleanPath(requestedPath);
|
|
65
|
+
const roots = artifactRoots(ctx);
|
|
66
|
+
if (roots.length === 0) throw httpError(400, "artifact_root_unavailable");
|
|
67
|
+
|
|
68
|
+
const variants = pathAliases(raw);
|
|
69
|
+
const absoluteVariants = variants.filter(isAbsoluteArtifactPath);
|
|
70
|
+
const candidates = (absoluteVariants.length > 0
|
|
71
|
+
? absoluteVariants
|
|
72
|
+
: roots.flatMap((root) => variants.map((variant) => join(root, variant))))
|
|
73
|
+
.map((candidate) => normalize(candidate));
|
|
74
|
+
|
|
75
|
+
const allowed = candidates.filter((candidate) => roots.some((root) => ensureInside(candidate, root)));
|
|
76
|
+
const path = allowed.find((candidate) => existsSync(candidate)) || allowed[0];
|
|
77
|
+
if (!path) throw httpError(400, "artifact_path_outside_allowed_roots");
|
|
78
|
+
return { path, roots };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeArtifactFileName(value, fallback = "artifact.md") {
|
|
82
|
+
const raw = cleanPath(value || fallback);
|
|
83
|
+
const name = basename(raw).replace(/[^A-Za-z0-9._-]+/g, "-") || fallback;
|
|
84
|
+
const ext = extname(name).toLowerCase();
|
|
85
|
+
if (!MARKDOWN_EXTENSIONS.has(ext)) throw httpError(415, "unsupported_artifact_type");
|
|
86
|
+
return name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function boardArtifactRoot(ctx) {
|
|
90
|
+
if (!ctx.board.db_path) throw httpError(400, "artifact_root_unavailable");
|
|
91
|
+
return normalize(join(dirname(ctx.board.db_path), "artifacts"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function storeTicketMarkdownArtifact(ticketId, body, ctx) {
|
|
95
|
+
const ticket = ticketById(ctx.db, ticketId);
|
|
96
|
+
if (!ticket || ticket.board_id !== ctx.board.id) throw httpError(404, "ticket_not_found");
|
|
97
|
+
requireBoardAccess(ctx.actor, boardById(ctx.db, ticket.board_id));
|
|
98
|
+
|
|
99
|
+
const content = String(body?.content || "");
|
|
100
|
+
const size = Buffer.byteLength(content, "utf8");
|
|
101
|
+
if (size > MAX_MARKDOWN_ARTIFACT_BYTES) throw httpError(413, "artifact_too_large");
|
|
102
|
+
|
|
103
|
+
const fileName = safeArtifactFileName(body?.filename || body?.path || "artifact.md");
|
|
104
|
+
const root = boardArtifactRoot(ctx);
|
|
105
|
+
const relativePath = `tickets/${ticket.id}/${fileName}`;
|
|
106
|
+
const absolutePath = normalize(join(root, relativePath));
|
|
107
|
+
if (!ensureInside(absolutePath, root)) throw httpError(400, "artifact_path_outside_allowed_roots");
|
|
108
|
+
|
|
109
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
110
|
+
writeFileSync(absolutePath, content, "utf8");
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
ticket_id: ticket.id,
|
|
114
|
+
path: relativePath,
|
|
115
|
+
absolute_path: absolutePath,
|
|
116
|
+
size_bytes: size
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function displayPathFor(path, roots) {
|
|
121
|
+
const root = roots
|
|
122
|
+
.filter((candidate) => ensureInside(path, candidate))
|
|
123
|
+
.sort((a, b) => normalize(b).length - normalize(a).length)[0];
|
|
124
|
+
if (!root) return path;
|
|
125
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
126
|
+
return rel || path;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getTicketMarkdownArtifact(ticketId, requestedPath, ctx) {
|
|
130
|
+
const ticket = ticketById(ctx.db, ticketId);
|
|
131
|
+
if (!ticket || ticket.board_id !== ctx.board.id) throw httpError(404, "ticket_not_found");
|
|
132
|
+
requireBoardAccess(ctx.actor, boardById(ctx.db, ticket.board_id));
|
|
133
|
+
|
|
134
|
+
const { path, roots } = resolveArtifactPath(ctx, requestedPath);
|
|
135
|
+
const ext = extname(path).toLowerCase();
|
|
136
|
+
if (!MARKDOWN_EXTENSIONS.has(ext)) throw httpError(415, "unsupported_artifact_type");
|
|
137
|
+
if (!existsSync(path)) throw httpError(404, "artifact_not_found");
|
|
138
|
+
const stat = statSync(path);
|
|
139
|
+
if (!stat.isFile()) throw httpError(400, "artifact_not_file");
|
|
140
|
+
if (stat.size > MAX_MARKDOWN_ARTIFACT_BYTES) throw httpError(413, "artifact_too_large");
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
ticket_id: ticket.id,
|
|
144
|
+
path: displayPathFor(path, roots),
|
|
145
|
+
absolute_path: path,
|
|
146
|
+
size_bytes: stat.size,
|
|
147
|
+
content: readFileSync(path, "utf8")
|
|
148
|
+
};
|
|
149
|
+
}
|
package/src/core/board.js
CHANGED
|
@@ -110,10 +110,10 @@ export const boardRuntime = {
|
|
|
110
110
|
port: PORT
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
export function startupSummary(port = PORT) {
|
|
113
|
+
export function startupSummary(port = PORT, host = "localhost") {
|
|
114
114
|
const rows = listBoards();
|
|
115
115
|
return {
|
|
116
|
-
url: `http
|
|
116
|
+
url: `http://${host}:${port}`,
|
|
117
117
|
registryPath: REGISTRY_DB_PATH,
|
|
118
118
|
boardCount: rows.length,
|
|
119
119
|
boardPaths: rows.map((row) => row.db_path)
|
|
@@ -140,6 +140,7 @@ export { archivedTicketsForBoard } from "./queries.js";
|
|
|
140
140
|
export { getTicketBlockers, getTicketRelations } from "./relations.js";
|
|
141
141
|
export { checkpointTicket, claimNext, completeTicket, getAgentDispatchPacket, getContextPack, getContextPackFull, readComments, readTicket } from "./agent.js";
|
|
142
142
|
export { searchTickets } from "./search.js";
|
|
143
|
+
export { getTicketMarkdownArtifact, storeTicketMarkdownArtifact } from "./artifacts.js";
|
|
143
144
|
export { exportBoard } from "./snapshots.js";
|
|
144
145
|
|
|
145
146
|
// Helpers for ad-hoc id/slug minting in callers (e.g. MCP tools that mint
|
package/src/core/boards.js
CHANGED
|
@@ -323,7 +323,6 @@ export function boardManual(boardId, ctx, options = {}) {
|
|
|
323
323
|
repo_url: innerBoard.repo_url,
|
|
324
324
|
system_path: innerBoard.system_path || ctx.board.repo_path || "",
|
|
325
325
|
default_branch: innerBoard.default_branch,
|
|
326
|
-
project_notes: innerBoard.project_notes,
|
|
327
326
|
agent_instructions: innerBoard.agent_instructions,
|
|
328
327
|
ai_enabled: innerBoard.ai_enabled,
|
|
329
328
|
manage_helper_files: ctx.board.manage_helper_files,
|
package/src/core/css-bundle.js
CHANGED
|
@@ -21,6 +21,7 @@ export async function bundleCss({ entryPoint, outfile, minify = false }) {
|
|
|
21
21
|
outfile,
|
|
22
22
|
assetNames: "cursors/[name]",
|
|
23
23
|
loader: CURSOR_ASSET_LOADERS,
|
|
24
|
+
external: ["./fonts/*", "fonts/*"],
|
|
24
25
|
legalComments: "none"
|
|
25
26
|
});
|
|
26
27
|
const cssFile = result.outputFiles.find((file) => file.path.endsWith(".css"));
|
package/src/core/db.js
CHANGED
|
@@ -172,7 +172,9 @@ export function createBoardSchema(db) {
|
|
|
172
172
|
type TEXT NOT NULL DEFAULT 'task',
|
|
173
173
|
parent_ticket_id TEXT,
|
|
174
174
|
ai_plan TEXT NOT NULL DEFAULT '',
|
|
175
|
+
plan_artifact_path TEXT NOT NULL DEFAULT '',
|
|
175
176
|
implementation_summary TEXT NOT NULL DEFAULT '',
|
|
177
|
+
implementation_artifact_path TEXT NOT NULL DEFAULT '',
|
|
176
178
|
implementation_updates TEXT NOT NULL DEFAULT '',
|
|
177
179
|
state_id TEXT NOT NULL REFERENCES states(id),
|
|
178
180
|
priority INTEGER NOT NULL DEFAULT 2,
|
|
@@ -189,6 +191,7 @@ export function createBoardSchema(db) {
|
|
|
189
191
|
author TEXT NOT NULL,
|
|
190
192
|
kind TEXT NOT NULL DEFAULT 'comment',
|
|
191
193
|
body TEXT NOT NULL,
|
|
194
|
+
omitted_for_ai INTEGER NOT NULL DEFAULT 0,
|
|
192
195
|
created_at TEXT NOT NULL
|
|
193
196
|
);
|
|
194
197
|
|
|
@@ -289,6 +292,9 @@ export function createBoardSchema(db) {
|
|
|
289
292
|
CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_ticket_id);
|
|
290
293
|
CREATE INDEX IF NOT EXISTS idx_events_ticket ON events(ticket_id, created_at);
|
|
291
294
|
`);
|
|
295
|
+
ensureColumn(db, "tickets", "plan_artifact_path", "TEXT NOT NULL DEFAULT ''");
|
|
296
|
+
ensureColumn(db, "tickets", "implementation_artifact_path", "TEXT NOT NULL DEFAULT ''");
|
|
297
|
+
ensureColumn(db, "comments", "omitted_for_ai", "INTEGER NOT NULL DEFAULT 0");
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
export function resetBoard(db) {
|