@starlove/orbit 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
package/src/core/states.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { boardById, compactStatePositions, stateById } from "./queries.js";
|
|
2
|
-
import { tx } from "./db.js";
|
|
3
|
-
import { requireBoardAccess, requirePermission } from "./auth.js";
|
|
4
|
-
import { recordEvent } from "./events.js";
|
|
5
|
-
import {
|
|
6
|
-
httpError,
|
|
7
|
-
id,
|
|
8
|
-
normalizeStateRole,
|
|
9
|
-
now,
|
|
10
|
-
requiredString
|
|
11
|
-
} from "./util.js";
|
|
12
|
-
|
|
13
|
-
export function createState(boardId, body, ctx) {
|
|
14
|
-
const { db, actor } = ctx;
|
|
15
|
-
const board = boardById(db, boardId);
|
|
16
|
-
if (!board) throw httpError(404, "board_not_found");
|
|
17
|
-
requireBoardAccess(actor, board);
|
|
18
|
-
requirePermission(actor, "write");
|
|
19
|
-
|
|
20
|
-
const role = body.role !== undefined ? normalizeStateRole(body.role) : null;
|
|
21
|
-
// role='ai_ready' implies canonical name; reject mismatches so the AI tab
|
|
22
|
-
// checkbox always lands a lane named exactly "AI Ready".
|
|
23
|
-
const name =
|
|
24
|
-
role === "ai_ready"
|
|
25
|
-
? body.name && body.name !== "AI Ready"
|
|
26
|
-
? (() => { throw httpError(409, "ai_ready_lane_must_be_named_ai_ready"); })()
|
|
27
|
-
: "AI Ready"
|
|
28
|
-
: requiredString(body.name, "name");
|
|
29
|
-
const stateId = id();
|
|
30
|
-
return tx(db, () => {
|
|
31
|
-
if (role) {
|
|
32
|
-
db.prepare("UPDATE states SET role = NULL WHERE board_id = ? AND role = ?").run(boardId, role);
|
|
33
|
-
}
|
|
34
|
-
const maxPosition = db.prepare("SELECT MAX(position) AS value FROM states WHERE board_id = ?").get(boardId).value;
|
|
35
|
-
db.prepare(
|
|
36
|
-
`INSERT INTO states
|
|
37
|
-
(id, board_id, name, position, is_default, role, created_at)
|
|
38
|
-
VALUES (?, ?, ?, ?, 0, ?, ?)`
|
|
39
|
-
).run(stateId, boardId, name, Number(maxPosition || 0) + 1, role, now());
|
|
40
|
-
recordEvent(db, boardId, "state_created", null, actor.name, {
|
|
41
|
-
board_id: boardId,
|
|
42
|
-
state_id: stateId,
|
|
43
|
-
name,
|
|
44
|
-
role
|
|
45
|
-
});
|
|
46
|
-
return stateById(db, stateId);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function updateState(stateId, body, ctx) {
|
|
51
|
-
const { db, actor } = ctx;
|
|
52
|
-
const before = stateById(db, stateId);
|
|
53
|
-
if (!before) throw httpError(404, "state_not_found");
|
|
54
|
-
const board = boardById(db, before.board_id);
|
|
55
|
-
requireBoardAccess(actor, board);
|
|
56
|
-
requirePermission(actor, "write");
|
|
57
|
-
|
|
58
|
-
return tx(db, () => {
|
|
59
|
-
const sets = [];
|
|
60
|
-
const values = [];
|
|
61
|
-
if (body.name !== undefined) {
|
|
62
|
-
const nextName = requiredString(body.name, "name");
|
|
63
|
-
// The AI Ready lane is the canonical anchor for agent claim-next. Locking
|
|
64
|
-
// its name removes a whole class of silent breakage; users who want a
|
|
65
|
-
// different label can disable the AI Ready column from Settings → AI.
|
|
66
|
-
if (before.role === "ai_ready" && nextName !== "AI Ready") {
|
|
67
|
-
throw httpError(409, "cannot_rename_ai_ready_lane");
|
|
68
|
-
}
|
|
69
|
-
sets.push("name = ?");
|
|
70
|
-
values.push(nextName);
|
|
71
|
-
}
|
|
72
|
-
if (body.is_default !== undefined) {
|
|
73
|
-
const nextDefault = body.is_default ? 1 : 0;
|
|
74
|
-
if (nextDefault) db.prepare("UPDATE states SET is_default = 0 WHERE board_id = ?").run(before.board_id);
|
|
75
|
-
sets.push("is_default = ?");
|
|
76
|
-
values.push(nextDefault);
|
|
77
|
-
}
|
|
78
|
-
if (body.role !== undefined) {
|
|
79
|
-
const nextRole = normalizeStateRole(body.role);
|
|
80
|
-
// Roles are unique per board: at most one lane plays each role. If another
|
|
81
|
-
// lane already holds it, clear that one first so the new assignment wins.
|
|
82
|
-
if (nextRole) {
|
|
83
|
-
db.prepare("UPDATE states SET role = NULL WHERE board_id = ? AND role = ? AND id != ?")
|
|
84
|
-
.run(before.board_id, nextRole, stateId);
|
|
85
|
-
}
|
|
86
|
-
// Assigning role='ai_ready' implies the lane name must be "AI Ready" (the
|
|
87
|
-
// canonical name agents key off). If the caller is also renaming via
|
|
88
|
-
// body.name above, that took precedence; otherwise force it here.
|
|
89
|
-
if (nextRole === "ai_ready") {
|
|
90
|
-
const finalName = body.name !== undefined ? requiredString(body.name, "name") : before.name;
|
|
91
|
-
if (finalName !== "AI Ready") {
|
|
92
|
-
if (body.name === undefined) {
|
|
93
|
-
// Caller didn't ask to rename — silently set the canonical name.
|
|
94
|
-
sets.push("name = ?");
|
|
95
|
-
values.push("AI Ready");
|
|
96
|
-
} else {
|
|
97
|
-
throw httpError(409, "ai_ready_lane_must_be_named_ai_ready");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
sets.push("role = ?");
|
|
102
|
-
values.push(nextRole);
|
|
103
|
-
}
|
|
104
|
-
if (sets.length === 0) return before;
|
|
105
|
-
|
|
106
|
-
values.push(stateId);
|
|
107
|
-
db.prepare(`UPDATE states SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
108
|
-
recordEvent(db, before.board_id, "state_updated", null, actor.name, {
|
|
109
|
-
board_id: before.board_id,
|
|
110
|
-
state_id: stateId,
|
|
111
|
-
fields: Object.keys(body)
|
|
112
|
-
});
|
|
113
|
-
return stateById(db, stateId);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function reorderStates(boardId, body, ctx) {
|
|
118
|
-
const { db, actor } = ctx;
|
|
119
|
-
const board = boardById(db, boardId);
|
|
120
|
-
if (!board) throw httpError(404, "board_not_found");
|
|
121
|
-
requireBoardAccess(actor, board);
|
|
122
|
-
requirePermission(actor, "write");
|
|
123
|
-
|
|
124
|
-
const ids = Array.isArray(body.state_ids) ? body.state_ids : [];
|
|
125
|
-
const existing = db.prepare("SELECT id FROM states WHERE board_id = ?").all(boardId).map((row) => row.id);
|
|
126
|
-
if (ids.length !== existing.length || !existing.every((stateId) => ids.includes(stateId))) {
|
|
127
|
-
throw httpError(400, "invalid_state_order");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return tx(db, () => {
|
|
131
|
-
ids.forEach((stateId, position) => {
|
|
132
|
-
db.prepare("UPDATE states SET position = ? WHERE id = ? AND board_id = ?").run(position, stateId, boardId);
|
|
133
|
-
});
|
|
134
|
-
recordEvent(db, boardId, "states_reordered", null, actor.name, { board_id: boardId, state_ids: ids });
|
|
135
|
-
return db.prepare("SELECT * FROM states WHERE board_id = ? ORDER BY position").all(boardId);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function deleteState(stateId, ctx) {
|
|
140
|
-
const { db, actor } = ctx;
|
|
141
|
-
const state = stateById(db, stateId);
|
|
142
|
-
if (!state) throw httpError(404, "state_not_found");
|
|
143
|
-
const board = boardById(db, state.board_id);
|
|
144
|
-
requireBoardAccess(actor, board);
|
|
145
|
-
requirePermission(actor, "write");
|
|
146
|
-
|
|
147
|
-
const ticketCount = db.prepare("SELECT COUNT(*) AS count FROM tickets WHERE state_id = ?").get(stateId).count;
|
|
148
|
-
if (ticketCount > 0) throw httpError(409, "state_has_tickets");
|
|
149
|
-
|
|
150
|
-
const stateCount = db.prepare("SELECT COUNT(*) AS count FROM states WHERE board_id = ?").get(state.board_id).count;
|
|
151
|
-
if (stateCount <= 1) throw httpError(409, "cannot_delete_last_state");
|
|
152
|
-
|
|
153
|
-
return tx(db, () => {
|
|
154
|
-
db.prepare("DELETE FROM states WHERE id = ?").run(stateId);
|
|
155
|
-
compactStatePositions(db, state.board_id);
|
|
156
|
-
if (state.is_default) {
|
|
157
|
-
const first = db.prepare("SELECT id FROM states WHERE board_id = ? ORDER BY position LIMIT 1").get(state.board_id);
|
|
158
|
-
if (first) db.prepare("UPDATE states SET is_default = 1 WHERE id = ?").run(first.id);
|
|
159
|
-
}
|
|
160
|
-
recordEvent(db, state.board_id, "state_deleted", null, actor.name, {
|
|
161
|
-
board_id: state.board_id,
|
|
162
|
-
state_id: stateId,
|
|
163
|
-
name: state.name
|
|
164
|
-
});
|
|
165
|
-
return { ok: true, deleted_state_id: stateId };
|
|
166
|
-
});
|
|
167
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { normalizePath } from "./util.js";
|
|
4
|
-
|
|
5
|
-
const execFileAsync = promisify(execFile);
|
|
6
|
-
|
|
7
|
-
const DEFAULT_TITLE = "Choose repo folder for Orbit board";
|
|
8
|
-
|
|
9
|
-
const WINDOWS_FOLDER_PICKER_ARGS = (title) => [
|
|
10
|
-
"-NoProfile",
|
|
11
|
-
"-STA",
|
|
12
|
-
"-Command",
|
|
13
|
-
[
|
|
14
|
-
"Add-Type -AssemblyName System.Windows.Forms",
|
|
15
|
-
"$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
16
|
-
`$dialog.Description = ${JSON.stringify(title)}`,
|
|
17
|
-
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {",
|
|
18
|
-
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
19
|
-
" Write-Output $dialog.SelectedPath",
|
|
20
|
-
"}"
|
|
21
|
-
].join("; ")
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export function folderPickerCommands(platform = process.platform, title = DEFAULT_TITLE) {
|
|
25
|
-
if (platform === "darwin") {
|
|
26
|
-
return [
|
|
27
|
-
{
|
|
28
|
-
command: "osascript",
|
|
29
|
-
args: ["-e", `POSIX path of (choose folder with prompt ${JSON.stringify(title)})`]
|
|
30
|
-
}
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (platform === "win32") {
|
|
35
|
-
return [
|
|
36
|
-
{
|
|
37
|
-
command: "powershell.exe",
|
|
38
|
-
args: WINDOWS_FOLDER_PICKER_ARGS(title)
|
|
39
|
-
}
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return [
|
|
44
|
-
{
|
|
45
|
-
command: "zenity",
|
|
46
|
-
args: ["--file-selection", "--directory", "--title", title]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
command: "kdialog",
|
|
50
|
-
args: ["--getexistingdirectory", ".", title]
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
command: "powershell.exe",
|
|
54
|
-
args: WINDOWS_FOLDER_PICKER_ARGS(title)
|
|
55
|
-
}
|
|
56
|
-
];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isMissingCommand(error) {
|
|
60
|
-
return error?.code === "ENOENT";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isPickerCancel(error) {
|
|
64
|
-
const stderr = String(error?.stderr || "");
|
|
65
|
-
return error?.code === 1 && /cancel|user canceled|user cancelled/i.test(stderr);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function windowsPathToWslPath(rawPath) {
|
|
69
|
-
const path = String(rawPath || "").trim();
|
|
70
|
-
const drivePath = path.match(/^([a-zA-Z]):[\\/](.*)$/);
|
|
71
|
-
if (!drivePath) return path;
|
|
72
|
-
const drive = drivePath[1].toLowerCase();
|
|
73
|
-
const rest = drivePath[2].replace(/\\/g, "/").replace(/^\/+/, "");
|
|
74
|
-
return `/mnt/${drive}/${rest}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function pickFolder(options = {}) {
|
|
78
|
-
const platform = options.platform || process.platform;
|
|
79
|
-
const title = options.title || DEFAULT_TITLE;
|
|
80
|
-
const execFileImpl = options.execFileImpl || execFileAsync;
|
|
81
|
-
const commands = folderPickerCommands(platform, title);
|
|
82
|
-
let missingCommands = 0;
|
|
83
|
-
|
|
84
|
-
for (const spec of commands) {
|
|
85
|
-
try {
|
|
86
|
-
const result = await execFileImpl(spec.command, spec.args, {
|
|
87
|
-
encoding: "utf8",
|
|
88
|
-
timeout: 120000,
|
|
89
|
-
windowsHide: false
|
|
90
|
-
});
|
|
91
|
-
const rawPath = String(result.stdout || "").trim();
|
|
92
|
-
const selectedPath = normalizePath(platform === "linux" && spec.command === "powershell.exe" ? windowsPathToWslPath(rawPath) : rawPath);
|
|
93
|
-
return selectedPath ? { path: selectedPath } : { canceled: true };
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (isMissingCommand(error)) {
|
|
96
|
-
missingCommands += 1;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (isPickerCancel(error)) return { canceled: true };
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (missingCommands === commands.length) {
|
|
105
|
-
return { unsupported: true };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return { canceled: true };
|
|
109
|
-
}
|
package/src/core/tickets.js
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addComment,
|
|
3
|
-
boardById,
|
|
4
|
-
defaultStateId,
|
|
5
|
-
ensureLabel,
|
|
6
|
-
ticketById
|
|
7
|
-
} from "./queries.js";
|
|
8
|
-
import { requireBoardAccess, requirePermission } from "./auth.js";
|
|
9
|
-
import { tx } from "./db.js";
|
|
10
|
-
import { recordEvent } from "./events.js";
|
|
11
|
-
import {
|
|
12
|
-
httpError,
|
|
13
|
-
id,
|
|
14
|
-
now,
|
|
15
|
-
normalizePriority,
|
|
16
|
-
normalizeTicketType,
|
|
17
|
-
requiredString
|
|
18
|
-
} from "./util.js";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Bump a ticket's `updated_at` without recording an event or touching other
|
|
22
|
-
* fields. Used to roll child activity up to the parent epic so the column sort
|
|
23
|
-
* (ORDER BY updated_at DESC) keeps the epic group near recent activity even
|
|
24
|
-
* when only its children changed. Pass `null`/undefined to no-op.
|
|
25
|
-
*/
|
|
26
|
-
export function bumpTicketUpdatedAt(db, ticketId, time) {
|
|
27
|
-
if (!ticketId) return;
|
|
28
|
-
db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createTicket(body, ctx) {
|
|
32
|
-
const { db, board, actor } = ctx;
|
|
33
|
-
// The router resolves `ctx.board` from path/body/query before we get here.
|
|
34
|
-
// If the caller also sent `body.board_id`, it must agree with ctx.board.
|
|
35
|
-
if (body.board_id && body.board_id !== board.id) {
|
|
36
|
-
throw httpError(400, "board_id_mismatch");
|
|
37
|
-
}
|
|
38
|
-
const boardId = board.id;
|
|
39
|
-
const innerBoard = boardById(db, boardId);
|
|
40
|
-
if (!innerBoard) throw httpError(404, "board_not_found");
|
|
41
|
-
requireBoardAccess(actor, innerBoard);
|
|
42
|
-
requirePermission(actor, "write");
|
|
43
|
-
|
|
44
|
-
const title = requiredString(body.title, "title");
|
|
45
|
-
const stateId = body.state_id || defaultStateId(db, boardId);
|
|
46
|
-
const state = db.prepare("SELECT * FROM states WHERE id = ? AND board_id = ?").get(stateId, boardId);
|
|
47
|
-
if (!state) throw httpError(400, "invalid_state");
|
|
48
|
-
|
|
49
|
-
const ticketType = normalizeTicketType(body.type || (body.parent_ticket_id ? "feature" : "task"));
|
|
50
|
-
const parentTicketId = normalizeParentTicketId(db, body.parent_ticket_id, boardId, ticketType);
|
|
51
|
-
const ticketId = id();
|
|
52
|
-
const time = now();
|
|
53
|
-
|
|
54
|
-
return tx(db, () => {
|
|
55
|
-
// MAX(number)+1 must be read and consumed inside the transaction: BEGIN
|
|
56
|
-
// IMMEDIATE holds the write lock so a concurrent process (e.g. an MCP
|
|
57
|
-
// agent creating a ticket) can't pick the same number and trip the
|
|
58
|
-
// UNIQUE(board_id, number) constraint.
|
|
59
|
-
const nextNumber =
|
|
60
|
-
(db.prepare("SELECT MAX(number) AS value FROM tickets WHERE board_id = ?").get(boardId).value || 0) + 1;
|
|
61
|
-
|
|
62
|
-
db.prepare(
|
|
63
|
-
`INSERT INTO tickets
|
|
64
|
-
(id, board_id, number, title, description, type, parent_ticket_id, ai_plan,
|
|
65
|
-
plan_artifact_path, implementation_summary, implementation_artifact_path,
|
|
66
|
-
implementation_updates, state_id, priority, created_by, created_at, updated_at)
|
|
67
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
68
|
-
).run(
|
|
69
|
-
ticketId,
|
|
70
|
-
boardId,
|
|
71
|
-
nextNumber,
|
|
72
|
-
title,
|
|
73
|
-
body.description || "",
|
|
74
|
-
ticketType,
|
|
75
|
-
parentTicketId,
|
|
76
|
-
body.ai_plan || "",
|
|
77
|
-
body.plan_artifact_path || "",
|
|
78
|
-
body.implementation_summary || "",
|
|
79
|
-
body.implementation_artifact_path || "",
|
|
80
|
-
body.implementation_updates || "",
|
|
81
|
-
stateId,
|
|
82
|
-
normalizePriority(body.priority),
|
|
83
|
-
actor.name,
|
|
84
|
-
time,
|
|
85
|
-
time
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
for (const labelName of body.labels || []) {
|
|
89
|
-
const label = ensureLabel(db, boardId, labelName);
|
|
90
|
-
db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticketId, label.id);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
recordEvent(db, boardId, "ticket_created", ticketId, actor.name, {
|
|
94
|
-
title,
|
|
95
|
-
type: ticketType,
|
|
96
|
-
parent_ticket_id: parentTicketId
|
|
97
|
-
});
|
|
98
|
-
reindexTicket(db, ticketId);
|
|
99
|
-
bumpTicketUpdatedAt(db, parentTicketId, time);
|
|
100
|
-
return ticketById(db, ticketId);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function updateTicket(ticketId, body, ctx) {
|
|
105
|
-
const { db, board, actor } = ctx;
|
|
106
|
-
const before = ticketById(db, ticketId);
|
|
107
|
-
if (!before) throw httpError(404, "ticket_not_found");
|
|
108
|
-
if (before.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
109
|
-
const innerBoard = boardById(db, before.board_id);
|
|
110
|
-
requireBoardAccess(actor, innerBoard);
|
|
111
|
-
requirePermission(actor, "write");
|
|
112
|
-
|
|
113
|
-
const allowed = [
|
|
114
|
-
"title",
|
|
115
|
-
"description",
|
|
116
|
-
"type",
|
|
117
|
-
"parent_ticket_id",
|
|
118
|
-
"ai_plan",
|
|
119
|
-
"plan_artifact_path",
|
|
120
|
-
"implementation_summary",
|
|
121
|
-
"implementation_artifact_path",
|
|
122
|
-
"implementation_updates",
|
|
123
|
-
"state_id",
|
|
124
|
-
"priority"
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
const nextType = body.type !== undefined ? normalizeTicketType(body.type) : before.type;
|
|
128
|
-
|
|
129
|
-
// Resolve every requested field to the value that will actually be written,
|
|
130
|
-
// without mutating the caller's request body. The same resolved map drives
|
|
131
|
-
// both the UPDATE and the audit event, so history reflects what was stored
|
|
132
|
-
// (normalized priority/type, validated parent) rather than raw input.
|
|
133
|
-
const applied = {};
|
|
134
|
-
for (const field of allowed) {
|
|
135
|
-
if (body[field] === undefined) continue;
|
|
136
|
-
if (field === "type") applied.type = nextType;
|
|
137
|
-
else if (field === "priority") applied.priority = normalizePriority(body.priority);
|
|
138
|
-
else if (field === "parent_ticket_id") {
|
|
139
|
-
applied.parent_ticket_id = normalizeParentTicketId(db, body.parent_ticket_id, before.board_id, nextType, before.id);
|
|
140
|
-
} else applied[field] = body[field];
|
|
141
|
-
}
|
|
142
|
-
// Promoting a ticket to epic clears any parent, even if the caller didn't
|
|
143
|
-
// mention parent_ticket_id (epics cannot have parents).
|
|
144
|
-
if (nextType === "epic" && applied.parent_ticket_id === undefined) {
|
|
145
|
-
applied.parent_ticket_id = null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const labelSync = Array.isArray(body.labels);
|
|
149
|
-
const fields = Object.keys(applied);
|
|
150
|
-
|
|
151
|
-
if (fields.length === 0 && !labelSync) return before;
|
|
152
|
-
|
|
153
|
-
if (applied.state_id) {
|
|
154
|
-
const state = db.prepare("SELECT * FROM states WHERE id = ? AND board_id = ?").get(applied.state_id, before.board_id);
|
|
155
|
-
if (!state) throw httpError(400, "invalid_state");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return tx(db, () => {
|
|
159
|
-
const time = now();
|
|
160
|
-
if (fields.length > 0) {
|
|
161
|
-
const sets = fields.map((field) => `${field} = ?`);
|
|
162
|
-
const values = fields.map((field) => applied[field]);
|
|
163
|
-
sets.push("updated_at = ?");
|
|
164
|
-
values.push(time, ticketId);
|
|
165
|
-
db.prepare(`UPDATE tickets SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Replace ticket labels when `labels` is sent (array of names). Labels are board-scoped.
|
|
169
|
-
if (labelSync) {
|
|
170
|
-
const boardScopeId = before.board_id;
|
|
171
|
-
db.prepare("DELETE FROM ticket_labels WHERE ticket_id = ?").run(ticketId);
|
|
172
|
-
for (const labelName of body.labels) {
|
|
173
|
-
const clean = String(labelName ?? "")
|
|
174
|
-
.trim()
|
|
175
|
-
.replace(/\s+/g, " ");
|
|
176
|
-
if (!clean) continue;
|
|
177
|
-
const label = ensureLabel(db, boardScopeId, clean);
|
|
178
|
-
db.prepare("INSERT OR IGNORE INTO ticket_labels (ticket_id, label_id) VALUES (?, ?)").run(ticketId, label.id);
|
|
179
|
-
}
|
|
180
|
-
if (fields.length === 0) {
|
|
181
|
-
db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const after = ticketById(db, ticketId);
|
|
186
|
-
if (before.state_id !== after.state_id) {
|
|
187
|
-
// Done lane is the auto-resolver for blocker relations. When a ticket
|
|
188
|
-
// lands in a role='done' lane, every relation that names this ticket as a
|
|
189
|
-
// blocker (`blocks` from this ticket, `blocked_by` toward this ticket)
|
|
190
|
-
// becomes meaningless — the relation row is the SSOT, so we delete it.
|
|
191
|
-
if (after.state_role === "done" && before.state_role !== "done") {
|
|
192
|
-
db.prepare(
|
|
193
|
-
`DELETE FROM relations
|
|
194
|
-
WHERE (source_ticket_id = ? AND type = 'blocks')
|
|
195
|
-
OR (target_ticket_id = ? AND type = 'blocked_by')`
|
|
196
|
-
).run(ticketId, ticketId);
|
|
197
|
-
}
|
|
198
|
-
recordEvent(db, before.board_id, "state_changed", ticketId, actor.name, {
|
|
199
|
-
from: before.state_name,
|
|
200
|
-
to: after.state_name,
|
|
201
|
-
actor_type: actor.type,
|
|
202
|
-
actor_id: actor.id
|
|
203
|
-
});
|
|
204
|
-
} else {
|
|
205
|
-
const eventPayload = { ...applied };
|
|
206
|
-
if (labelSync) eventPayload.labels = body.labels;
|
|
207
|
-
if (Object.keys(eventPayload).length > 0) {
|
|
208
|
-
recordEvent(db, before.board_id, "ticket_updated", ticketId, actor.name, eventPayload);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
reindexTicket(db, ticketId);
|
|
212
|
-
// Roll activity up to the parent epic so the column sort keeps the epic
|
|
213
|
-
// group near recent child activity. On reparent, bump both old and new
|
|
214
|
-
// parent — both views changed (one lost a child, the other gained one).
|
|
215
|
-
bumpTicketUpdatedAt(db, before.parent_ticket_id, time);
|
|
216
|
-
if (after.parent_ticket_id && after.parent_ticket_id !== before.parent_ticket_id) {
|
|
217
|
-
bumpTicketUpdatedAt(db, after.parent_ticket_id, time);
|
|
218
|
-
}
|
|
219
|
-
return after;
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function archiveTicket(ticketId, ctx) {
|
|
224
|
-
const { db, board, actor } = ctx;
|
|
225
|
-
const ticket = ticketById(db, ticketId);
|
|
226
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
227
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
228
|
-
requirePermission(actor, "write");
|
|
229
|
-
|
|
230
|
-
if (ticket.archived_at) return ticket;
|
|
231
|
-
|
|
232
|
-
return tx(db, () => {
|
|
233
|
-
const time = now();
|
|
234
|
-
db.prepare("UPDATE tickets SET archived_at = ?, updated_at = ? WHERE id = ?").run(time, time, ticketId);
|
|
235
|
-
recordEvent(db, ticket.board_id, "ticket_archived", ticketId, actor.name, { title: ticket.title });
|
|
236
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
237
|
-
return ticketById(db, ticketId);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function restoreTicket(ticketId, ctx) {
|
|
242
|
-
const { db, board, actor } = ctx;
|
|
243
|
-
const ticket = ticketById(db, ticketId);
|
|
244
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
245
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
246
|
-
requirePermission(actor, "write");
|
|
247
|
-
|
|
248
|
-
if (!ticket.archived_at) return ticket;
|
|
249
|
-
|
|
250
|
-
return tx(db, () => {
|
|
251
|
-
const time = now();
|
|
252
|
-
db.prepare("UPDATE tickets SET archived_at = NULL, updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
253
|
-
recordEvent(db, ticket.board_id, "ticket_restored", ticketId, actor.name, { title: ticket.title });
|
|
254
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
255
|
-
return ticketById(db, ticketId);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function deleteTicket(ticketId, ctx) {
|
|
260
|
-
const { db, board, actor } = ctx;
|
|
261
|
-
const ticket = ticketById(db, ticketId);
|
|
262
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
263
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
264
|
-
requirePermission(actor, "write");
|
|
265
|
-
|
|
266
|
-
if (!ticket.archived_at) throw httpError(409, "ticket_not_archived");
|
|
267
|
-
|
|
268
|
-
return tx(db, () => {
|
|
269
|
-
recordEvent(db, ticket.board_id, "ticket_deleted", ticketId, actor.name, {
|
|
270
|
-
title: ticket.title,
|
|
271
|
-
type: ticket.type,
|
|
272
|
-
board_id: ticket.board_id
|
|
273
|
-
});
|
|
274
|
-
db.prepare("DELETE FROM ticket_fts WHERE ticket_id = ?").run(ticketId);
|
|
275
|
-
db.prepare("DELETE FROM tickets WHERE id = ?").run(ticketId);
|
|
276
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, now());
|
|
277
|
-
return { ok: true, deleted_id: ticketId };
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function createComment(ticketId, body, ctx) {
|
|
282
|
-
const { db, board, actor } = ctx;
|
|
283
|
-
const ticket = ticketById(db, ticketId);
|
|
284
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
285
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
286
|
-
requirePermission(actor, "write");
|
|
287
|
-
|
|
288
|
-
const commentBody = requiredString(body.body, "body");
|
|
289
|
-
const author = body.author || actor.name;
|
|
290
|
-
const kind = body.kind || (actor.type === "agent" ? "agent_note" : "human_comment");
|
|
291
|
-
|
|
292
|
-
return tx(db, () => {
|
|
293
|
-
const time = now();
|
|
294
|
-
const comment = addComment(db, ticketId, author, kind, commentBody);
|
|
295
|
-
db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
296
|
-
recordEvent(db, ticket.board_id, "comment_created", ticketId, actor.name, { kind: comment.kind });
|
|
297
|
-
reindexTicket(db, ticketId);
|
|
298
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
299
|
-
return comment;
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function updateCommentAiOmission(ticketId, commentId, body, ctx) {
|
|
304
|
-
const { db, board, actor } = ctx;
|
|
305
|
-
const ticket = ticketById(db, ticketId);
|
|
306
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
307
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
308
|
-
requirePermission(actor, "write");
|
|
309
|
-
|
|
310
|
-
const comment = db.prepare("SELECT * FROM comments WHERE id = ? AND ticket_id = ?").get(commentId, ticketId);
|
|
311
|
-
if (!comment) throw httpError(404, "comment_not_found");
|
|
312
|
-
const omittedForAi = body.omitted_for_ai ?? body.omittedForAi;
|
|
313
|
-
if (typeof omittedForAi !== "boolean") throw httpError(400, "omitted_for_ai_required");
|
|
314
|
-
|
|
315
|
-
return tx(db, () => {
|
|
316
|
-
const time = now();
|
|
317
|
-
db.prepare("UPDATE comments SET omitted_for_ai = ? WHERE id = ?").run(omittedForAi ? 1 : 0, commentId);
|
|
318
|
-
db.prepare("UPDATE tickets SET updated_at = ? WHERE id = ?").run(time, ticketId);
|
|
319
|
-
recordEvent(db, ticket.board_id, "comment_ai_omission_changed", ticketId, actor.name, {
|
|
320
|
-
comment_id: commentId,
|
|
321
|
-
omitted_for_ai: omittedForAi
|
|
322
|
-
});
|
|
323
|
-
reindexTicket(db, ticketId);
|
|
324
|
-
bumpTicketUpdatedAt(db, ticket.parent_ticket_id, time);
|
|
325
|
-
return db.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export function getTicketStatusHistory(ticketId, ctx) {
|
|
330
|
-
const { db, board, actor } = ctx;
|
|
331
|
-
const ticket = ticketById(db, ticketId);
|
|
332
|
-
if (!ticket || ticket.board_id !== board.id) throw httpError(404, "ticket_not_found");
|
|
333
|
-
requireBoardAccess(actor, boardById(db, ticket.board_id));
|
|
334
|
-
|
|
335
|
-
return db
|
|
336
|
-
.prepare(
|
|
337
|
-
`SELECT * FROM events
|
|
338
|
-
WHERE ticket_id = ? AND type IN ('state_changed', 'checkpoint_requested', 'agent_completed')
|
|
339
|
-
ORDER BY created_at ASC`
|
|
340
|
-
)
|
|
341
|
-
.all(ticketId)
|
|
342
|
-
.map((e) => ({ ...e, body: JSON.parse(e.body_json) }));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function normalizeParentTicketId(db, value, boardId, ticketType, currentTicketId = "") {
|
|
346
|
-
if (value === undefined || value === null || value === "") {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
if (ticketType === "epic") throw httpError(400, "epic_cannot_have_parent");
|
|
350
|
-
if (value === currentTicketId) throw httpError(400, "ticket_cannot_parent_itself");
|
|
351
|
-
|
|
352
|
-
const parent = ticketById(db, value);
|
|
353
|
-
if (!parent || parent.board_id !== boardId) throw httpError(400, "invalid_parent_ticket");
|
|
354
|
-
|
|
355
|
-
// Walk the full ancestor chain, not just one hop: parenting onto `parent`
|
|
356
|
-
// must not make `currentTicketId` its own ancestor (A→B→C→A). The depth cap
|
|
357
|
-
// also stops a pre-existing corrupt cycle from looping forever here.
|
|
358
|
-
const seen = new Set();
|
|
359
|
-
let ancestor = parent;
|
|
360
|
-
let guard = 0;
|
|
361
|
-
while (ancestor) {
|
|
362
|
-
if (currentTicketId && ancestor.id === currentTicketId) {
|
|
363
|
-
throw httpError(400, "ticket_parent_cycle");
|
|
364
|
-
}
|
|
365
|
-
if (!ancestor.parent_ticket_id || seen.has(ancestor.parent_ticket_id) || guard++ > 1000) break;
|
|
366
|
-
seen.add(ancestor.parent_ticket_id);
|
|
367
|
-
ancestor = ticketById(db, ancestor.parent_ticket_id);
|
|
368
|
-
}
|
|
369
|
-
return parent.id;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function reindexTicket(db, ticketId) {
|
|
373
|
-
const ticket = db.prepare("SELECT * FROM tickets WHERE id = ?").get(ticketId);
|
|
374
|
-
if (!ticket) return;
|
|
375
|
-
const comments = db
|
|
376
|
-
.prepare("SELECT body FROM comments WHERE ticket_id = ? AND COALESCE(omitted_for_ai, 0) = 0 ORDER BY created_at ASC")
|
|
377
|
-
.all(ticketId)
|
|
378
|
-
.map((comment) => comment.body)
|
|
379
|
-
.join("\n");
|
|
380
|
-
const implementationText = [
|
|
381
|
-
`Type: ${ticket.type}`,
|
|
382
|
-
ticket.ai_plan,
|
|
383
|
-
ticket.implementation_summary,
|
|
384
|
-
ticket.implementation_updates
|
|
385
|
-
]
|
|
386
|
-
.filter(Boolean)
|
|
387
|
-
.join("\n");
|
|
388
|
-
db.prepare("DELETE FROM ticket_fts WHERE ticket_id = ?").run(ticketId);
|
|
389
|
-
db.prepare("INSERT INTO ticket_fts (ticket_id, title, description, comments) VALUES (?, ?, ?, ?)").run(
|
|
390
|
-
ticketId,
|
|
391
|
-
ticket.title,
|
|
392
|
-
ticket.description,
|
|
393
|
-
[implementationText, comments].filter(Boolean).join("\n")
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export function reindexAllTickets(db) {
|
|
398
|
-
const rows = db.prepare("SELECT id FROM tickets").all();
|
|
399
|
-
for (const row of rows) reindexTicket(db, row.id);
|
|
400
|
-
}
|