@starlove/orbit 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.62905fcb.js +0 -952
- package/dist/full/js/app.62905fcb.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 -229
- 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/agent.js
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addComment,
|
|
3
|
-
boardById,
|
|
4
|
-
childTicketsFor,
|
|
5
|
-
compactTicket,
|
|
6
|
-
labelsForTicket,
|
|
7
|
-
relationRows,
|
|
8
|
-
stateByName,
|
|
9
|
-
stateByRole,
|
|
10
|
-
stateById,
|
|
11
|
-
ticketById,
|
|
12
|
-
ticketByNumber,
|
|
13
|
-
ticketByTitle,
|
|
14
|
-
uniqueTickets,
|
|
15
|
-
unresolvedBlockers
|
|
16
|
-
} from "./queries.js";
|
|
17
|
-
import { canAccessBoard, requireBoardAccess } from "./auth.js";
|
|
18
|
-
import { tx } from "./db.js";
|
|
19
|
-
import { recordEvent } from "./events.js";
|
|
20
|
-
import {
|
|
21
|
-
appendFieldNote,
|
|
22
|
-
httpError,
|
|
23
|
-
now,
|
|
24
|
-
normalizeTicketType,
|
|
25
|
-
requiredString
|
|
26
|
-
} from "./util.js";
|
|
27
|
-
import { boardManual } from "./boards.js";
|
|
28
|
-
import { bumpTicketUpdatedAt, reindexTicket } from "./tickets.js";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Claim the next AI-ready ticket on the resolved board. The router/MCP layer
|
|
32
|
-
* is responsible for picking which board this runs against (via path slug,
|
|
33
|
-
* body field, or session board); this function never scans other boards or
|
|
34
|
-
* mutates global state. If you want to claim from another board, the caller
|
|
35
|
-
* resolves a different ctx and calls again.
|
|
36
|
-
*/
|
|
37
|
-
export function claimNext(body, ctx) {
|
|
38
|
-
const { db, board, actor } = ctx;
|
|
39
|
-
const filters = {
|
|
40
|
-
type: body.type ? normalizeTicketType(body.type) : "",
|
|
41
|
-
includeEpics: Boolean(body.include_epics)
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const inner = boardById(db, board.id);
|
|
45
|
-
if (!inner || !canAccessBoard(actor, inner)) {
|
|
46
|
-
throw httpError(403, "forbidden");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const candidates = db
|
|
50
|
-
.prepare(
|
|
51
|
-
`SELECT t.*, b.slug AS board_slug
|
|
52
|
-
FROM tickets t
|
|
53
|
-
JOIN states s ON s.id = t.state_id
|
|
54
|
-
JOIN boards b ON b.id = t.board_id
|
|
55
|
-
WHERE (s.role = 'ai_ready' OR (s.role IS NULL AND s.name = 'AI Ready'))
|
|
56
|
-
AND t.archived_at IS NULL
|
|
57
|
-
ORDER BY t.priority DESC, t.created_at ASC`
|
|
58
|
-
)
|
|
59
|
-
.all();
|
|
60
|
-
|
|
61
|
-
const filtered = candidates.filter((ticket) =>
|
|
62
|
-
!filters.type ? filters.includeEpics || ticket.type !== "epic" : ticket.type === filters.type
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
for (const candidate of filtered) {
|
|
66
|
-
const blockedBy = unresolvedBlockers(db, candidate.id);
|
|
67
|
-
if (blockedBy.length > 0) continue;
|
|
68
|
-
|
|
69
|
-
const inProgress =
|
|
70
|
-
stateByRole(db, candidate.board_id, "in_progress") ||
|
|
71
|
-
stateByName(db, candidate.board_id, "In Progress");
|
|
72
|
-
if (!inProgress) throw httpError(400, "missing_in_progress_state");
|
|
73
|
-
|
|
74
|
-
tx(db, () => {
|
|
75
|
-
const time = now();
|
|
76
|
-
db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(
|
|
77
|
-
inProgress.id,
|
|
78
|
-
time,
|
|
79
|
-
candidate.id
|
|
80
|
-
);
|
|
81
|
-
addComment(db, candidate.id, actor.name, "agent_note", "Agent claimed this ticket and is starting work.");
|
|
82
|
-
recordEvent(db, candidate.board_id, "agent_claimed", candidate.id, actor.name, { agent_id: actor.id });
|
|
83
|
-
reindexTicket(db, candidate.id);
|
|
84
|
-
bumpTicketUpdatedAt(db, candidate.parent_ticket_id, time);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
claimed: true,
|
|
89
|
-
ticket_id: candidate.id,
|
|
90
|
-
context: readTicket(candidate.id, ctx)
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { claimed: false, reason: "no_schedulable_ticket" };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resolveTicketLookup(db, boardId, { ticket_id, number, title }) {
|
|
98
|
-
if (ticket_id) return ticketById(db, ticket_id);
|
|
99
|
-
if (number !== undefined && number !== null && number !== "") {
|
|
100
|
-
const n = Number(number);
|
|
101
|
-
if (!Number.isInteger(n)) throw httpError(400, "invalid_number");
|
|
102
|
-
return ticketByNumber(db, boardId, n);
|
|
103
|
-
}
|
|
104
|
-
if (title) return ticketByTitle(db, boardId, String(title));
|
|
105
|
-
throw httpError(400, "ticket_id_or_number_or_title_required");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function readTicket(lookup, ctx) {
|
|
109
|
-
const { db, board, actor } = ctx;
|
|
110
|
-
const args = typeof lookup === "string" ? { ticket_id: lookup } : lookup || {};
|
|
111
|
-
const ticket = resolveTicketLookup(db, board.id, args);
|
|
112
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
113
|
-
const innerBoard = boardById(db, ticket.board_id);
|
|
114
|
-
requireBoardAccess(actor, innerBoard);
|
|
115
|
-
|
|
116
|
-
const labels = labelsForTicket(db, ticket.id);
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
ticket: {
|
|
120
|
-
id: ticket.id,
|
|
121
|
-
number: ticket.number,
|
|
122
|
-
title: ticket.title,
|
|
123
|
-
description: ticket.description,
|
|
124
|
-
state_name: ticket.state_name,
|
|
125
|
-
type: ticket.type,
|
|
126
|
-
priority: ticket.priority,
|
|
127
|
-
labels
|
|
128
|
-
},
|
|
129
|
-
board_manual: boardManual(innerBoard.id, ctx)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function readComments(lookup, ctx) {
|
|
134
|
-
const { db, board, actor } = ctx;
|
|
135
|
-
const args = typeof lookup === "string" ? { ticket_id: lookup } : lookup || {};
|
|
136
|
-
const ticket = resolveTicketLookup(db, board.id, args);
|
|
137
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
138
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
139
|
-
const comments = db
|
|
140
|
-
.prepare("SELECT * FROM comments WHERE ticket_id = ? ORDER BY created_at ASC")
|
|
141
|
-
.all(ticket.id);
|
|
142
|
-
return { ticket_id: ticket.id, comments };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function checkpointTicket(body, ctx) {
|
|
146
|
-
const { db, board, actor } = ctx;
|
|
147
|
-
const tid = requiredString(body.ticket_id, "ticket_id");
|
|
148
|
-
const ticket = ticketById(db, tid);
|
|
149
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
150
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
151
|
-
|
|
152
|
-
// Same review lane as `complete`; distinguish mid-flight pause via checkpoint comment + event kind.
|
|
153
|
-
const review =
|
|
154
|
-
stateByRole(db, ticket.board_id, "review") ||
|
|
155
|
-
stateByName(db, ticket.board_id, "Review");
|
|
156
|
-
if (!review) throw httpError(400, "missing_review_state");
|
|
157
|
-
const message = requiredString(body.message, "message");
|
|
158
|
-
tx(db, () => {
|
|
159
|
-
const time = now();
|
|
160
|
-
db.prepare("UPDATE tickets SET state_id = ?, updated_at = ? WHERE id = ?").run(review.id, time, ticket.id);
|
|
161
|
-
addComment(db, ticket.id, actor.name, "checkpoint", message);
|
|
162
|
-
recordEvent(db, ticket.board_id, "checkpoint_requested", ticket.id, actor.name, {
|
|
163
|
-
from: ticket.state_name,
|
|
164
|
-
state: review.name,
|
|
165
|
-
to: review.name,
|
|
166
|
-
actor_type: actor.type,
|
|
167
|
-
actor_id: actor.id
|
|
168
|
-
});
|
|
169
|
-
reindexTicket(db, ticket.id);
|
|
170
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
171
|
-
});
|
|
172
|
-
return getContextPack(ticket.id, ctx, 1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function completeTicket(body, ctx) {
|
|
176
|
-
const { db, board, actor } = ctx;
|
|
177
|
-
const tid = requiredString(body.ticket_id, "ticket_id");
|
|
178
|
-
const ticket = ticketById(db, tid);
|
|
179
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
180
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
181
|
-
|
|
182
|
-
// If the caller specified a next_state, honor that name verbatim. Otherwise
|
|
183
|
-
// resolve the review lane by role so renaming the lane does not break this.
|
|
184
|
-
const nextState = body.next_state
|
|
185
|
-
? stateByName(db, ticket.board_id, body.next_state)
|
|
186
|
-
: stateByRole(db, ticket.board_id, "review") || stateByName(db, ticket.board_id, "Review");
|
|
187
|
-
if (!nextState) throw httpError(400, "missing_next_state");
|
|
188
|
-
|
|
189
|
-
const summary = requiredString(body.summary, "summary");
|
|
190
|
-
const lines = [summary];
|
|
191
|
-
if (body.pr_url) lines.push(`PR: ${body.pr_url}`);
|
|
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 || "";
|
|
194
|
-
tx(db, () => {
|
|
195
|
-
const time = now();
|
|
196
|
-
addComment(db, ticket.id, actor.name, "completion", lines.join("\n\n"));
|
|
197
|
-
db.prepare(
|
|
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);
|
|
203
|
-
recordEvent(db, ticket.board_id, "agent_completed", ticket.id, actor.name, {
|
|
204
|
-
from: ticket.state_name,
|
|
205
|
-
next_state: nextState.name,
|
|
206
|
-
to: nextState.name,
|
|
207
|
-
pr_url: body.pr_url || "",
|
|
208
|
-
actor_type: actor.type,
|
|
209
|
-
actor_id: actor.id
|
|
210
|
-
});
|
|
211
|
-
reindexTicket(db, ticket.id);
|
|
212
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
213
|
-
});
|
|
214
|
-
return getContextPack(ticket.id, ctx, 1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function contextBoardIdentity(innerBoard, ctx) {
|
|
218
|
-
return {
|
|
219
|
-
id: innerBoard.id,
|
|
220
|
-
slug: innerBoard.slug,
|
|
221
|
-
name: innerBoard.name,
|
|
222
|
-
repo_path: innerBoard.system_path || ctx.board.repo_path || "",
|
|
223
|
-
system_path: innerBoard.system_path || ctx.board.repo_path || "",
|
|
224
|
-
default_branch: innerBoard.default_branch || "main"
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function buildContextPack(ticketId, ctx, depth = 1, options = {}, { includeProjectContext = false } = {}) {
|
|
229
|
-
const { db, board, actor } = ctx;
|
|
230
|
-
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 0);
|
|
231
|
-
const includeParentFull = Boolean(options.include_parent_full ?? options.includeParentFull);
|
|
232
|
-
const includeRelatedFull = Boolean(options.include_related_full ?? options.includeRelatedFull);
|
|
233
|
-
const ticket = ticketById(db, ticketId);
|
|
234
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
235
|
-
const innerBoard = boardById(db, ticket.board_id);
|
|
236
|
-
requireBoardAccess(actor, innerBoard);
|
|
237
|
-
|
|
238
|
-
const labels = labelsForTicket(db, ticketId);
|
|
239
|
-
const relations = relationRows(db, ticketId);
|
|
240
|
-
const blockers = unresolvedBlockers(db, ticketId);
|
|
241
|
-
const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
|
|
242
|
-
const childTickets = childTicketsFor(db, ticketId).map((child) => ({
|
|
243
|
-
...child,
|
|
244
|
-
labels: labelsForTicket(db, child.id),
|
|
245
|
-
blockers: unresolvedBlockers(db, child.id)
|
|
246
|
-
}));
|
|
247
|
-
|
|
248
|
-
const relatedTickets = uniqueTickets([
|
|
249
|
-
...relations.map((relation) => relation.other_ticket),
|
|
250
|
-
...childTickets,
|
|
251
|
-
...(parentTicket ? [parentTicket] : [])
|
|
252
|
-
]);
|
|
253
|
-
|
|
254
|
-
const pack = {
|
|
255
|
-
ticket: capTicketFields({
|
|
256
|
-
...ticket,
|
|
257
|
-
labels
|
|
258
|
-
}, maxCharsPerField),
|
|
259
|
-
board: contextBoardIdentity(innerBoard, ctx),
|
|
260
|
-
parent_ticket: parentTicket ? (includeParentFull ? capTicketFields({ ...parentTicket, labels: labelsForTicket(db, parentTicket.id) }, maxCharsPerField) : compactTicket(parentTicket)) : null,
|
|
261
|
-
child_tickets: childTickets.map((child) => ({
|
|
262
|
-
...capTicketFields(child, maxCharsPerField),
|
|
263
|
-
child_count: childTicketsFor(db, child.id).length
|
|
264
|
-
})),
|
|
265
|
-
relations: relations.map((relation) => ({
|
|
266
|
-
...relation,
|
|
267
|
-
other_ticket: includeRelatedFull
|
|
268
|
-
? capTicketFields({ ...relation.other_ticket, labels: labelsForTicket(db, relation.other_ticket.id) }, maxCharsPerField)
|
|
269
|
-
: compactTicket(relation.other_ticket)
|
|
270
|
-
})),
|
|
271
|
-
blockers,
|
|
272
|
-
related_tickets: relatedTickets.map((related) =>
|
|
273
|
-
includeRelatedFull ? capTicketFields({ ...related, labels: labelsForTicket(db, related.id) }, maxCharsPerField) : compactTicket(related)
|
|
274
|
-
)
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
if (includeProjectContext) pack.board_manual = boardManual(innerBoard.id, ctx);
|
|
278
|
-
return pack;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function getContextPack(ticketId, ctx, depth = 1, options = {}) {
|
|
282
|
-
return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: false });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function getContextPackFull(ticketId, ctx, depth = 1, options = {}) {
|
|
286
|
-
return buildContextPack(ticketId, ctx, depth, options, { includeProjectContext: true });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function getAgentDispatchPacket(ticketId, ctx, options = {}) {
|
|
290
|
-
const { db, board, actor } = ctx;
|
|
291
|
-
const maxCharsPerField = positiveInt(options.max_chars_per_field ?? options.maxCharsPerField, 4000);
|
|
292
|
-
const commentLimit = Math.min(nonNegativeInt(options.comment_limit ?? options.commentLimit, 5), 20);
|
|
293
|
-
const ticket = ticketById(db, ticketId);
|
|
294
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
295
|
-
const innerBoard = boardById(db, ticket.board_id);
|
|
296
|
-
requireBoardAccess(actor, innerBoard);
|
|
297
|
-
|
|
298
|
-
const labels = labelsForTicket(db, ticket.id).map((label) => label.name);
|
|
299
|
-
const parentTicket = ticket.parent_ticket_id ? ticketById(db, ticket.parent_ticket_id) : null;
|
|
300
|
-
const blockers = unresolvedBlockers(db, ticket.id).map(compactTicket);
|
|
301
|
-
const comments = db
|
|
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 ?")
|
|
303
|
-
.all(ticket.id, commentLimit)
|
|
304
|
-
.reverse()
|
|
305
|
-
.map((comment) => ({ ...comment, body: capString(comment.body, maxCharsPerField) }));
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
board: {
|
|
309
|
-
id: innerBoard.id,
|
|
310
|
-
slug: innerBoard.slug,
|
|
311
|
-
name: innerBoard.name,
|
|
312
|
-
repo_path: innerBoard.repo_path || innerBoard.system_path || "",
|
|
313
|
-
system_path: innerBoard.system_path || innerBoard.repo_path || "",
|
|
314
|
-
default_branch: innerBoard.default_branch || "main",
|
|
315
|
-
agent_instructions: capString(innerBoard.agent_instructions || "", maxCharsPerField)
|
|
316
|
-
},
|
|
317
|
-
ticket: {
|
|
318
|
-
id: ticket.id,
|
|
319
|
-
ticket_id: ticket.id,
|
|
320
|
-
number: ticket.number,
|
|
321
|
-
title: ticket.title,
|
|
322
|
-
type: ticket.type,
|
|
323
|
-
priority: ticket.priority,
|
|
324
|
-
state_id: ticket.state_id,
|
|
325
|
-
state_name: ticket.state_name,
|
|
326
|
-
state_role: ticket.state_role,
|
|
327
|
-
description: capString(ticket.description || "", maxCharsPerField),
|
|
328
|
-
acceptance: capString(extractAcceptance(ticket.description || ""), maxCharsPerField),
|
|
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),
|
|
334
|
-
labels
|
|
335
|
-
},
|
|
336
|
-
blockers: {
|
|
337
|
-
can_start: blockers.length === 0,
|
|
338
|
-
blockers
|
|
339
|
-
},
|
|
340
|
-
parent_ticket: parentTicket ? compactTicket(parentTicket) : null,
|
|
341
|
-
relevant_state_ids: relevantStateIds(db, innerBoard.id, ticket.state_id),
|
|
342
|
-
recent_comments: comments
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function positiveInt(value, fallback) {
|
|
347
|
-
const n = Number(value);
|
|
348
|
-
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function nonNegativeInt(value, fallback) {
|
|
352
|
-
const n = Number(value);
|
|
353
|
-
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function capString(value, maxChars) {
|
|
357
|
-
const text = String(value || "");
|
|
358
|
-
return maxChars && text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function capTicketFields(ticket, maxChars) {
|
|
362
|
-
if (!maxChars) return ticket;
|
|
363
|
-
const copy = { ...ticket };
|
|
364
|
-
for (const field of ["description", "ai_plan", "implementation_summary", "implementation_updates"]) {
|
|
365
|
-
if (Object.hasOwn(copy, field)) copy[field] = capString(copy[field], maxChars);
|
|
366
|
-
}
|
|
367
|
-
return copy;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function extractAcceptance(description) {
|
|
371
|
-
const match = String(description || "").match(/(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i);
|
|
372
|
-
return match ? match[1].trim() : "";
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function relevantStateIds(db, boardId, currentStateId) {
|
|
376
|
-
const byRole = (role) => stateByRole(db, boardId, role)?.id || null;
|
|
377
|
-
const byName = (name) => stateByName(db, boardId, name)?.id || null;
|
|
378
|
-
return {
|
|
379
|
-
current: currentStateId,
|
|
380
|
-
current_role: stateById(db, currentStateId)?.role || null,
|
|
381
|
-
ai_ready: byRole("ai_ready") || byName("AI Ready"),
|
|
382
|
-
in_progress: byRole("in_progress") || byName("In Progress"),
|
|
383
|
-
review: byRole("review") || byName("Review"),
|
|
384
|
-
done: byRole("done") || byName("Done"),
|
|
385
|
-
todo: byRole("todo") || byName("Todo")
|
|
386
|
-
};
|
|
387
|
-
}
|
package/src/core/agents-md.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export const ORBIT_AGENTS_START = "<!--ORBIT-->";
|
|
5
|
-
export const ORBIT_AGENTS_END = "<!--/ORBIT-->";
|
|
6
|
-
export const ORBIT_AGENTS_CANONICAL_LINE =
|
|
7
|
-
"Git is canonical for code. `SKILL-ORBIT.md` is canonical for Orbit/kanban/ticket/card workflow.";
|
|
8
|
-
|
|
9
|
-
export function orbitAgentsSection() {
|
|
10
|
-
return `${ORBIT_AGENTS_START}
|
|
11
|
-
${ORBIT_AGENTS_CANONICAL_LINE}
|
|
12
|
-
|
|
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
|
-
Use Orbit API/MCP tools for tickets/cards; do not edit .orbit/board.db directly.
|
|
15
|
-
${ORBIT_AGENTS_END}\n`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function buildAgentsMd() {
|
|
19
|
-
return `# AGENTS.md
|
|
20
|
-
|
|
21
|
-
${orbitAgentsSection()}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
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
|
-
/**
|
|
37
|
-
* Write both Orbit helper files (SKILL-ORBIT.md + the AGENTS.md Orbit section)
|
|
38
|
-
* into a project root. This is the shared core behind `orbit init` and the
|
|
39
|
-
* web-UI "Generate AI helper files" checkbox, so the two flows stay identical.
|
|
40
|
-
* Never throws — returns a result the caller can log or ignore.
|
|
41
|
-
*/
|
|
42
|
-
export function generateHelperFiles(projectRoot, skillSrc, logger = console.log) {
|
|
43
|
-
if (!existsSync(skillSrc)) {
|
|
44
|
-
return { ok: false, status: "missing_source", source: skillSrc };
|
|
45
|
-
}
|
|
46
|
-
const skill = syncSkillOrbitMd(projectRoot, skillSrc);
|
|
47
|
-
syncAgentsMd(projectRoot, {}, logger);
|
|
48
|
-
return { ok: true, status: skill.status, skill };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function syncAgentsMd(projectRoot, _options = {}, logger = console.log) {
|
|
52
|
-
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
53
|
-
const section = orbitAgentsSection();
|
|
54
|
-
|
|
55
|
-
if (!existsSync(agentsPath)) {
|
|
56
|
-
writeFileSync(agentsPath, buildAgentsMd(), "utf8");
|
|
57
|
-
logger(`Wrote ${agentsPath}`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const current = readFileSync(agentsPath, "utf8");
|
|
62
|
-
const startIndex = current.indexOf(ORBIT_AGENTS_START);
|
|
63
|
-
const endIndex = current.indexOf(ORBIT_AGENTS_END);
|
|
64
|
-
|
|
65
|
-
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
66
|
-
logger("AGENTS.md already includes Orbit instructions — left unchanged.");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
writeFileSync(agentsPath, `${current.replace(/\s*$/, "\n\n")}${section}`, "utf8");
|
|
71
|
-
logger(`Appended Orbit instructions to ${agentsPath}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function removeExactLegacyIntro(before) {
|
|
75
|
-
const escaped = ORBIT_AGENTS_CANONICAL_LINE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
|
-
const legacyIntro = new RegExp(`(?:^|\\n)[ \\t]*${escaped}[ \\t]*(?:\\r?\\n)?[ \\t]*$`);
|
|
77
|
-
return before.replace(legacyIntro, (match) => (match.startsWith("\n") ? "\n" : ""));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function joinAfterAgentsRemoval(beforeRaw, afterRaw) {
|
|
81
|
-
const before = removeExactLegacyIntro(beforeRaw).replace(/\s*$/, "");
|
|
82
|
-
const after = afterRaw.replace(/^\s*/, "");
|
|
83
|
-
if (before && after) return `${before}\n\n${after}`;
|
|
84
|
-
if (before) return `${before}\n`;
|
|
85
|
-
return after;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function removeOrbitAgentsSection(projectRoot) {
|
|
89
|
-
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
90
|
-
if (!existsSync(agentsPath)) {
|
|
91
|
-
return { ok: true, removed: false, path: agentsPath, reason: "agents_md_missing" };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const current = readFileSync(agentsPath, "utf8");
|
|
95
|
-
const startMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_START, "g"))];
|
|
96
|
-
const endMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_END, "g"))];
|
|
97
|
-
if (startMatches.length === 0 && endMatches.length === 0) {
|
|
98
|
-
return { ok: true, removed: false, path: agentsPath, reason: "orbit_section_missing" };
|
|
99
|
-
}
|
|
100
|
-
if (startMatches.length !== 1 || endMatches.length !== 1 || endMatches[0].index <= startMatches[0].index) {
|
|
101
|
-
return { ok: false, removed: false, path: agentsPath, reason: "ambiguous_orbit_section" };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const startIndex = startMatches[0].index;
|
|
105
|
-
const endIndex = endMatches[0].index + ORBIT_AGENTS_END.length;
|
|
106
|
-
const next = joinAfterAgentsRemoval(current.slice(0, startIndex), current.slice(endIndex));
|
|
107
|
-
if (next !== current) writeFileSync(agentsPath, next, "utf8");
|
|
108
|
-
return { ok: true, removed: next !== current, path: agentsPath };
|
|
109
|
-
}
|
package/src/core/artifacts.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
}
|