@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.
Files changed (129) hide show
  1. package/Dockerfile +2 -1
  2. package/README.md +10 -9
  3. package/SKILL-ORBIT.md +41 -17
  4. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  5. package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  6. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  7. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  8. package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  9. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  10. package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  11. package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  12. package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  13. package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  14. package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
  15. package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  16. package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  17. package/dist/full/fonts/Inter/OFL.txt +93 -0
  18. package/dist/full/fonts/Inter/README.txt +118 -0
  19. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  20. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  21. package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
  22. package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
  23. package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
  24. package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
  25. package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  26. package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  27. package/dist/full/fonts/Open_Sans/README.txt +100 -0
  28. package/dist/full/fonts/Orbitron/OFL.txt +93 -0
  29. package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  30. package/dist/full/fonts/Orbitron/README.txt +68 -0
  31. package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
  32. package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  33. package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  34. package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  35. package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  36. package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  37. package/dist/full/fonts/runescape-uf.ttf +0 -0
  38. package/dist/full/index.html +135 -25
  39. package/dist/full/js/app.4e6fbe20.js +952 -0
  40. package/dist/full/js/app.4e6fbe20.js.map +7 -0
  41. package/dist/full/styles.4e60d824.css +1 -0
  42. package/dist/full/version.json +2 -2
  43. package/docs/AGENT_PROTOCOL.md +20 -13
  44. package/docs/ORBIT_DISPATCH.md +73 -38
  45. package/docs/UPDATING.md +114 -0
  46. package/package.json +5 -7
  47. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  48. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  49. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  50. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  51. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  52. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  53. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  54. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  55. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  56. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  57. package/public/fonts/Chakra_Petch/OFL.txt +93 -0
  58. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  59. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  60. package/public/fonts/Inter/OFL.txt +93 -0
  61. package/public/fonts/Inter/README.txt +118 -0
  62. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  63. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  64. package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
  65. package/public/fonts/JetBrains_Mono/README.txt +79 -0
  66. package/public/fonts/OpenRing-Regular.ttf +0 -0
  67. package/public/fonts/Open_Sans/OFL.txt +93 -0
  68. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  69. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  70. package/public/fonts/Open_Sans/README.txt +100 -0
  71. package/public/fonts/Orbitron/OFL.txt +93 -0
  72. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  73. package/public/fonts/Orbitron/README.txt +68 -0
  74. package/public/fonts/Rajdhani/OFL.txt +93 -0
  75. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  76. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  77. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  78. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  79. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  80. package/public/fonts/runescape-uf.ttf +0 -0
  81. package/public/index.html +133 -23
  82. package/public/js/artifact-drawer.js +79 -0
  83. package/public/js/board-export.js +161 -0
  84. package/public/js/board-menu.js +219 -17
  85. package/public/js/card-actions.js +1 -1
  86. package/public/js/create-card.js +60 -23
  87. package/public/js/drawer.js +164 -4
  88. package/public/js/font-preference.js +230 -0
  89. package/public/js/format.js +0 -10
  90. package/public/js/icons.js +16 -0
  91. package/public/js/local-backend.js +31 -4
  92. package/public/js/main.js +7 -1
  93. package/public/js/modal.js +3 -1
  94. package/public/js/panel.js +58 -0
  95. package/public/js/settings-tabs.js +1 -1
  96. package/public/js/settings.js +606 -275
  97. package/public/js/sse.js +3 -0
  98. package/public/js/state.js +1 -1
  99. package/public/js/text-editing.js +128 -0
  100. package/public/js/ticket-detail.js +377 -148
  101. package/public/js/ticket-typeahead.js +49 -0
  102. package/public/js/toast.js +19 -4
  103. package/public/js/topbar-notes.js +3 -3
  104. package/public/js/typeahead.js +206 -0
  105. package/public/js/update-check.js +58 -0
  106. package/public/styles.css +5447 -3572
  107. package/src/cli/orbit.js +9 -5
  108. package/src/core/agent.js +12 -4
  109. package/src/core/agents-md.js +3 -3
  110. package/src/core/artifacts.js +149 -0
  111. package/src/core/board.js +3 -2
  112. package/src/core/boards.js +0 -1
  113. package/src/core/css-bundle.js +1 -0
  114. package/src/core/db.js +6 -0
  115. package/src/core/dispatch.js +344 -34
  116. package/src/core/http.js +9 -2
  117. package/src/core/paths.js +26 -2
  118. package/src/core/router.js +33 -0
  119. package/src/core/search.js +3 -2
  120. package/src/core/snapshots.js +10 -5
  121. package/src/core/tickets.js +34 -3
  122. package/src/core/util.js +3 -2
  123. package/src/core/version-check.js +154 -0
  124. package/src/mcp/orbit-client.js +18 -2
  125. package/src/mcp-server.js +19 -14
  126. package/src/server.js +28 -3
  127. package/dist/full/js/app.4ae80764.js +0 -742
  128. package/dist/full/js/app.4ae80764.js.map +0 -7
  129. 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> Refused for now; dispatch is local-board only
45
- --remote Refused for now; use remote MCP/manual orchestration for hosted boards
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 { dispatchTicket } = await import("../core/dispatch.js");
605
- const result = dispatchTicket(options);
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 Written-Plan updated with the generated handoff.");
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
- "UPDATE tickets SET state_id = ?, implementation_summary = ?, implementation_updates = ?, updated_at = ? WHERE id = ?"
198
- ).run(nextState.id, summary, updates, time, ticket.id);
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: includeProjectContext ? innerBoard : contextBoardIdentity(innerBoard, ctx),
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: {
@@ -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 = "<!-- ORBIT:AGENTS-START -->";
5
- export const ORBIT_AGENTS_END = "<!-- 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
- When work mentions Orbit, kanban, board, lane, ticket, card, epic, blocker, claim, AI Ready, implementation fields, planning state, project memory, or handoff: read \`SKILL-ORBIT.md\` first and follow it.
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://localhost:${port}`,
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
@@ -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,
@@ -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) {