@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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is asking. Orbit has no accounts: a person in the app is the owner, and
|
|
3
|
+
* an agent says its name (ORBIT_AGENT_NAME, or x-orbit-agent-name over HTTP).
|
|
4
|
+
* A host with accounts passes its own actors instead.
|
|
5
|
+
*/
|
|
6
|
+
import type { Actor } from "./model.ts";
|
|
7
|
+
/** The person using the app on their own Orbit. */
|
|
8
|
+
export declare function localOwnerActor(): Actor;
|
|
9
|
+
/** An agent that didn't say its name. */
|
|
10
|
+
export declare function localAgentActor(): Actor;
|
|
11
|
+
/** An agent that says its name. */
|
|
12
|
+
export declare function namedAgentActor(name: unknown): Actor;
|
|
13
|
+
/**
|
|
14
|
+
* The actor a request's headers say: an agent when it says it is one
|
|
15
|
+
* (`x-orbit-actor-type: agent`) or names itself (`x-orbit-agent-name`), else
|
|
16
|
+
* the owner.
|
|
17
|
+
*/
|
|
18
|
+
export declare function actorFromHeaders(header: (name: string) => string | undefined): Actor;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is asking. Orbit has no accounts: a person in the app is the owner, and
|
|
3
|
+
* an agent says its name (ORBIT_AGENT_NAME, or x-orbit-agent-name over HTTP).
|
|
4
|
+
* A host with accounts passes its own actors instead.
|
|
5
|
+
*/
|
|
6
|
+
import { asText } from "./text.js";
|
|
7
|
+
const ALL = ["owner", "read", "write", "claim"];
|
|
8
|
+
/** The person using the app on their own Orbit. */
|
|
9
|
+
export function localOwnerActor() {
|
|
10
|
+
return { id: "local", name: "local", type: "human", role: "owner", permissions: [...ALL], boards: ["*"] };
|
|
11
|
+
}
|
|
12
|
+
/** An agent that didn't say its name. */
|
|
13
|
+
export function localAgentActor() {
|
|
14
|
+
return { id: "local-agent", name: "agent", type: "agent", role: "owner", permissions: [...ALL], boards: ["*"] };
|
|
15
|
+
}
|
|
16
|
+
function cleanField(value, fallback) {
|
|
17
|
+
const clean = asText(value).trim().replace(/\s+/g, " ").slice(0, 80);
|
|
18
|
+
return clean || fallback;
|
|
19
|
+
}
|
|
20
|
+
function idFromName(name) {
|
|
21
|
+
return (name
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "")
|
|
25
|
+
.slice(0, 80) || "agent");
|
|
26
|
+
}
|
|
27
|
+
/** An agent that says its name. */
|
|
28
|
+
export function namedAgentActor(name) {
|
|
29
|
+
const clean = cleanField(name, "agent");
|
|
30
|
+
return { ...localAgentActor(), id: idFromName(clean), name: clean };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The actor a request's headers say: an agent when it says it is one
|
|
34
|
+
* (`x-orbit-actor-type: agent`) or names itself (`x-orbit-agent-name`), else
|
|
35
|
+
* the owner.
|
|
36
|
+
*/
|
|
37
|
+
export function actorFromHeaders(header) {
|
|
38
|
+
const type = (header("x-orbit-actor-type") ?? "").trim().toLowerCase();
|
|
39
|
+
const agentName = header("x-orbit-agent-name") ?? "";
|
|
40
|
+
if (type !== "agent" && !agentName.trim())
|
|
41
|
+
return localOwnerActor();
|
|
42
|
+
const name = cleanField(header("x-orbit-actor-name") || agentName, "agent");
|
|
43
|
+
const id = cleanField(header("x-orbit-actor-id"), idFromName(name));
|
|
44
|
+
return { ...localAgentActor(), id, name };
|
|
45
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent flow: an agent takes a card (the next AI Ready one, or one it is
|
|
3
|
+
* named), reads it with the board's rules, asks when the goal is unclear, and
|
|
4
|
+
* finishes into Review. Context packs give an agent a card with what is
|
|
5
|
+
* around it, capped to what it can afford to read.
|
|
6
|
+
*/
|
|
7
|
+
import { boardManual } from "./boards.ts";
|
|
8
|
+
import type { BoardManual } from "./boards.ts";
|
|
9
|
+
import type { CrossCard } from "./cross-board.ts";
|
|
10
|
+
import type { BoardCtx } from "./context.ts";
|
|
11
|
+
import type { Comment, Id, Label, Ticket } from "./model.ts";
|
|
12
|
+
import type { CompactTicket, LanedTicket, TicketView } from "./views.ts";
|
|
13
|
+
import type { BoardReader } from "../store/types.ts";
|
|
14
|
+
/** The comment Orbit writes on every claim. */
|
|
15
|
+
export declare const CLAIM_NOTE = "Agent claimed this ticket and is starting work.";
|
|
16
|
+
export interface Lookup {
|
|
17
|
+
ticket_id?: unknown;
|
|
18
|
+
number?: unknown;
|
|
19
|
+
title?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** A card by id, number or exact title (any case), on this board, or null. */
|
|
22
|
+
export declare function lookupTicket(r: BoardReader, lookup: Lookup): Promise<Ticket | null>;
|
|
23
|
+
export interface ReadTicket {
|
|
24
|
+
ticket: {
|
|
25
|
+
id: Id;
|
|
26
|
+
number: number;
|
|
27
|
+
title: string;
|
|
28
|
+
description: string;
|
|
29
|
+
state_name: string;
|
|
30
|
+
type: Ticket["type"];
|
|
31
|
+
priority: Ticket["priority"];
|
|
32
|
+
labels: Label[];
|
|
33
|
+
};
|
|
34
|
+
board_manual: BoardManual;
|
|
35
|
+
}
|
|
36
|
+
/** A card's title, description, lane and labels, with the board's rules and journal. */
|
|
37
|
+
export declare function readTicket(c: BoardCtx, lookup: Lookup): Promise<ReadTicket>;
|
|
38
|
+
/** A card's comments, oldest first, each with whether it is hidden from agents; an agent never gets a hidden one. */
|
|
39
|
+
export declare function readComments(c: BoardCtx, lookup: Lookup): Promise<{
|
|
40
|
+
ticket_id: Id;
|
|
41
|
+
comments: Comment[];
|
|
42
|
+
}>;
|
|
43
|
+
export type ClaimAnswer = {
|
|
44
|
+
claimed: true;
|
|
45
|
+
ticket_id: Id;
|
|
46
|
+
context: ReadTicket;
|
|
47
|
+
} | {
|
|
48
|
+
claimed: false;
|
|
49
|
+
reason: "no_schedulable_ticket";
|
|
50
|
+
} | {
|
|
51
|
+
claimed: false;
|
|
52
|
+
reason: "blocked";
|
|
53
|
+
ticket: CompactTicket;
|
|
54
|
+
blockers: CompactTicket[];
|
|
55
|
+
} | {
|
|
56
|
+
claimed: false;
|
|
57
|
+
reason: "already_done";
|
|
58
|
+
ticket: CompactTicket;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Takes the next card waiting in AI Ready on this board: highest priority,
|
|
62
|
+
* then oldest, skipping epics (unless asked) and blocked cards. Moves it to
|
|
63
|
+
* In Progress with a note.
|
|
64
|
+
*/
|
|
65
|
+
export declare function claimNext(c: BoardCtx, input?: {
|
|
66
|
+
type?: unknown;
|
|
67
|
+
include_epics?: unknown;
|
|
68
|
+
}): Promise<ClaimAnswer>;
|
|
69
|
+
/**
|
|
70
|
+
* Takes one named card. A blocked or done card is refused unless `force`. A
|
|
71
|
+
* card already In Progress stays where it is; with `becomeHolder` (the
|
|
72
|
+
* command language's `take`) the caller becomes its holder, noting a takeover.
|
|
73
|
+
*/
|
|
74
|
+
export declare function claimTicket(c: BoardCtx, input: Lookup & {
|
|
75
|
+
force?: unknown;
|
|
76
|
+
}, options?: {
|
|
77
|
+
becomeHolder?: boolean;
|
|
78
|
+
}): Promise<ClaimAnswer>;
|
|
79
|
+
/** Pauses for a person: the card goes to Review with the question as a checkpoint comment. */
|
|
80
|
+
export declare function checkpointTicket(c: BoardCtx, input: {
|
|
81
|
+
ticket_id?: unknown;
|
|
82
|
+
message?: unknown;
|
|
83
|
+
}): Promise<ContextPack>;
|
|
84
|
+
export interface CompleteInput {
|
|
85
|
+
ticket_id?: unknown;
|
|
86
|
+
summary?: unknown;
|
|
87
|
+
updates?: unknown;
|
|
88
|
+
pr_url?: unknown;
|
|
89
|
+
next_state?: unknown;
|
|
90
|
+
implementation_artifact_path?: unknown;
|
|
91
|
+
summary_artifact_path?: unknown;
|
|
92
|
+
}
|
|
93
|
+
/** Hands finished work to a person: the card goes to Review (or the lane named) with the summary saved. */
|
|
94
|
+
export declare function completeTicket(c: BoardCtx, input: CompleteInput): Promise<ContextPack>;
|
|
95
|
+
export interface ContextOptions {
|
|
96
|
+
max_chars_per_field?: unknown;
|
|
97
|
+
include_parent_full?: unknown;
|
|
98
|
+
include_related_full?: unknown;
|
|
99
|
+
comment_limit?: unknown;
|
|
100
|
+
}
|
|
101
|
+
export interface ContextPack {
|
|
102
|
+
ticket: TicketView & {
|
|
103
|
+
labels: Label[];
|
|
104
|
+
};
|
|
105
|
+
board: {
|
|
106
|
+
id: Id;
|
|
107
|
+
slug: string;
|
|
108
|
+
name: string;
|
|
109
|
+
default_branch: string;
|
|
110
|
+
};
|
|
111
|
+
parent_ticket: unknown;
|
|
112
|
+
child_tickets: unknown[];
|
|
113
|
+
relations: unknown[];
|
|
114
|
+
blockers: LanedTicket[];
|
|
115
|
+
/** Cards on other boards this card waits on; present only when there are some. */
|
|
116
|
+
waiting_on?: CrossCard[];
|
|
117
|
+
related_tickets: unknown[];
|
|
118
|
+
board_manual?: BoardManual;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A card with what is around it: its board, epic, sub-cards (with their
|
|
122
|
+
* labels and blockers), relations, blockers and related cards. `full` adds
|
|
123
|
+
* the board's rules and journal.
|
|
124
|
+
*/
|
|
125
|
+
export declare function contextPack(c: BoardCtx, ticketId: Id, options?: ContextOptions, full?: boolean): Promise<ContextPack>;
|
|
126
|
+
/** A lean packet for handing one card to an agent: capped text, blockers, the lanes it will move through, recent visible comments. */
|
|
127
|
+
export declare function dispatchPacket(c: BoardCtx, ticketId: Id, options?: ContextOptions): Promise<Record<string, unknown>>;
|
|
128
|
+
export { boardManual };
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent flow: an agent takes a card (the next AI Ready one, or one it is
|
|
3
|
+
* named), reads it with the board's rules, asks when the goal is unclear, and
|
|
4
|
+
* finishes into Review. Context packs give an agent a card with what is
|
|
5
|
+
* around it, capped to what it can afford to read.
|
|
6
|
+
*/
|
|
7
|
+
import { boardManual, manualOf } from "./boards.js";
|
|
8
|
+
import { cardLinkFromInput } from "./card-paths.js";
|
|
9
|
+
import { crossBlockers, crossBlockersFor } from "./cross-board.js";
|
|
10
|
+
import { isAgent } from "./context.js";
|
|
11
|
+
import { OrbitError } from "./errors.js";
|
|
12
|
+
import { record, reindex, touchTicket } from "./history.js";
|
|
13
|
+
import { normalizeTicketType, appendNote, cap, nonNegativeInt, positiveInt, requiredString, asText } from "./text.js";
|
|
14
|
+
import { ticketOn } from "./tickets.js";
|
|
15
|
+
import { Lanes, compactTicket, currentHolder, isHolder, labelsByTicket, laned, relationRows, ticketView, unresolvedBlockers } from "./views.js";
|
|
16
|
+
/** Another board's blocking card, in the short form a claim's answer lists blockers in. */
|
|
17
|
+
function compactCross(card) {
|
|
18
|
+
return { id: card.id, board_id: card.board_id, board_slug: card.board_slug, number: card.number, title: card.title, type: card.type, state_name: card.state_name, state_role: card.state_role, priority: card.priority };
|
|
19
|
+
}
|
|
20
|
+
/** The comment Orbit writes on every claim. */
|
|
21
|
+
export const CLAIM_NOTE = "Agent claimed this ticket and is starting work.";
|
|
22
|
+
/** A card by id, number or exact title (any case), on this board, or null. */
|
|
23
|
+
export async function lookupTicket(r, lookup) {
|
|
24
|
+
if (lookup.ticket_id)
|
|
25
|
+
return r.tickets.get(asText(lookup.ticket_id));
|
|
26
|
+
if (lookup.number !== undefined && lookup.number !== null && lookup.number !== "") {
|
|
27
|
+
const n = Number(lookup.number);
|
|
28
|
+
if (!Number.isInteger(n))
|
|
29
|
+
throw new OrbitError("invalid_number");
|
|
30
|
+
return r.tickets.byNumber(n);
|
|
31
|
+
}
|
|
32
|
+
if (lookup.title)
|
|
33
|
+
return r.tickets.byTitle(asText(lookup.title));
|
|
34
|
+
throw new OrbitError("ticket_id_or_number_or_title_required");
|
|
35
|
+
}
|
|
36
|
+
/** A card's title, description, lane and labels, with the board's rules and journal. */
|
|
37
|
+
export async function readTicket(c, lookup) {
|
|
38
|
+
return c.data.read(async (r) => {
|
|
39
|
+
const ticket = await lookupTicket(r, lookup);
|
|
40
|
+
if (!ticket)
|
|
41
|
+
throw new OrbitError("ticket_not_found");
|
|
42
|
+
const [lanes, labels, board, entries] = await Promise.all([Lanes.of(r), labelsByTicket(r, [ticket.id]), r.board.get(), r.entries.list()]);
|
|
43
|
+
return {
|
|
44
|
+
ticket: {
|
|
45
|
+
id: ticket.id,
|
|
46
|
+
number: ticket.number,
|
|
47
|
+
title: ticket.title,
|
|
48
|
+
description: ticket.description,
|
|
49
|
+
state_name: lanes.nameOf(ticket),
|
|
50
|
+
type: ticket.type,
|
|
51
|
+
priority: ticket.priority,
|
|
52
|
+
labels: labels.get(ticket.id) ?? []
|
|
53
|
+
},
|
|
54
|
+
board_manual: manualOf(c, board, entries)
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** A card's comments, oldest first, each with whether it is hidden from agents; an agent never gets a hidden one. */
|
|
59
|
+
export async function readComments(c, lookup) {
|
|
60
|
+
return c.data.read(async (r) => {
|
|
61
|
+
const ticket = await lookupTicket(r, lookup);
|
|
62
|
+
if (!ticket)
|
|
63
|
+
throw new OrbitError("ticket_not_found");
|
|
64
|
+
const comments = await r.comments.list(ticket.id);
|
|
65
|
+
return { ticket_id: ticket.id, comments: isAgent(c.actor) ? comments.filter((comment) => !comment.omitted_for_ai) : comments };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function inProgressLane(lanes) {
|
|
69
|
+
const lane = lanes.forRole("in_progress", "In Progress");
|
|
70
|
+
if (!lane)
|
|
71
|
+
throw new OrbitError("missing_in_progress_state");
|
|
72
|
+
return Promise.resolve(lane);
|
|
73
|
+
}
|
|
74
|
+
async function noteComment(w, c, ticketId, kind, body, at) {
|
|
75
|
+
await w.comments.insert({ id: c.ids.next(), ticket_id: ticketId, author: c.actor.name, kind, body, omitted_for_ai: 0, created_at: at });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Takes the next card waiting in AI Ready on this board: highest priority,
|
|
79
|
+
* then oldest, skipping epics (unless asked) and blocked cards. Moves it to
|
|
80
|
+
* In Progress with a note.
|
|
81
|
+
*/
|
|
82
|
+
export async function claimNext(c, input = {}) {
|
|
83
|
+
const type = input.type ? normalizeTicketType(input.type) : "";
|
|
84
|
+
const includeEpics = Boolean(input.include_epics);
|
|
85
|
+
const claimed = await c.data.write(async (w) => {
|
|
86
|
+
const lanes = await Lanes.of(w);
|
|
87
|
+
const ready = lanes.list.filter((state) => state.role === "ai_ready" || (state.role === null && state.name === "AI Ready"));
|
|
88
|
+
const candidates = [];
|
|
89
|
+
for (const lane of ready)
|
|
90
|
+
candidates.push(...(await w.tickets.list({ state: lane.id, archived: false })));
|
|
91
|
+
candidates.sort((a, b) => b.priority - a.priority || a.created_at.localeCompare(b.created_at) || a.number - b.number);
|
|
92
|
+
// Cards waiting on another board's card: one read per other board, for every candidate at once.
|
|
93
|
+
const elsewhere = await crossBlockers(c, await w.crossBoardRelations.list());
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
if (type ? candidate.type !== type : !includeEpics && candidate.type === "epic")
|
|
96
|
+
continue;
|
|
97
|
+
if ((await unresolvedBlockers(w, candidate.id, lanes)).length || elsewhere.get(candidate.id)?.length)
|
|
98
|
+
continue;
|
|
99
|
+
const inProgress = await inProgressLane(lanes);
|
|
100
|
+
const at = c.clock.now();
|
|
101
|
+
await w.tickets.update(candidate.id, { state_id: inProgress.id, updated_at: at });
|
|
102
|
+
await noteComment(w, c, candidate.id, "agent_note", CLAIM_NOTE, at);
|
|
103
|
+
await record(w, c, "agent_claimed", candidate.id, { agent_id: c.actor.id, from: lanes.nameOf(candidate), to: inProgress.name });
|
|
104
|
+
await reindex(w, candidate.id);
|
|
105
|
+
await touchTicket(w, candidate.parent_ticket_id, at);
|
|
106
|
+
return candidate.id;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
});
|
|
110
|
+
if (!claimed)
|
|
111
|
+
return { claimed: false, reason: "no_schedulable_ticket" };
|
|
112
|
+
c.changed(c.board.id);
|
|
113
|
+
return { claimed: true, ticket_id: claimed, context: await readTicket(c, { ticket_id: claimed }) };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Takes one named card. A blocked or done card is refused unless `force`. A
|
|
117
|
+
* card already In Progress stays where it is; with `becomeHolder` (the
|
|
118
|
+
* command language's `take`) the caller becomes its holder, noting a takeover.
|
|
119
|
+
*/
|
|
120
|
+
export async function claimTicket(c, input, options = {}) {
|
|
121
|
+
const force = Boolean(input.force);
|
|
122
|
+
const result = await c.data.write(async (w) => {
|
|
123
|
+
const ticket = await lookupTicket(w, input);
|
|
124
|
+
if (!ticket || ticket.archived_at)
|
|
125
|
+
throw new OrbitError("ticket_not_found");
|
|
126
|
+
const lanes = await Lanes.of(w);
|
|
127
|
+
const view = { ...laned(ticket, lanes), board_slug: c.board.slug };
|
|
128
|
+
const blockers = [...(await unresolvedBlockers(w, ticket.id, lanes)).map(compactTicket), ...(await crossBlockersFor(c, w, ticket.id)).map(compactCross)];
|
|
129
|
+
if (blockers.length && !force)
|
|
130
|
+
return { claimed: false, reason: "blocked", ticket: compactTicket(view), blockers };
|
|
131
|
+
if (lanes.roleOf(ticket) === "done" && !force)
|
|
132
|
+
return { claimed: false, reason: "already_done", ticket: compactTicket(view) };
|
|
133
|
+
const inProgress = await inProgressLane(lanes);
|
|
134
|
+
const at = c.clock.now();
|
|
135
|
+
if (ticket.state_id !== inProgress.id) {
|
|
136
|
+
await w.tickets.update(ticket.id, { state_id: inProgress.id, updated_at: at });
|
|
137
|
+
await noteComment(w, c, ticket.id, "agent_note", CLAIM_NOTE, at);
|
|
138
|
+
await record(w, c, "agent_claimed", ticket.id, { agent_id: c.actor.id, from: lanes.nameOf(ticket), to: inProgress.name });
|
|
139
|
+
await reindex(w, ticket.id);
|
|
140
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
141
|
+
return { claimed: true, ticket_id: ticket.id, wrote: true };
|
|
142
|
+
}
|
|
143
|
+
if (options.becomeHolder) {
|
|
144
|
+
const holder = await currentHolder(w, ticket.id);
|
|
145
|
+
if (!isHolder(holder, c.actor)) {
|
|
146
|
+
await w.tickets.update(ticket.id, { updated_at: at });
|
|
147
|
+
if (holder)
|
|
148
|
+
await noteComment(w, c, ticket.id, "agent_note", `Took over from @${holder.name}.`, at);
|
|
149
|
+
await record(w, c, "agent_claimed", ticket.id, { agent_id: c.actor.id, from: lanes.nameOf(ticket), to: inProgress.name, ...(holder ? { took_over_from: holder.name } : {}) });
|
|
150
|
+
await reindex(w, ticket.id);
|
|
151
|
+
return { claimed: true, ticket_id: ticket.id, wrote: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { claimed: true, ticket_id: ticket.id, wrote: false };
|
|
155
|
+
});
|
|
156
|
+
if (!result.claimed)
|
|
157
|
+
return result;
|
|
158
|
+
if ("wrote" in result && result.wrote)
|
|
159
|
+
c.changed(c.board.id);
|
|
160
|
+
return { claimed: true, ticket_id: result.ticket_id, context: await readTicket(c, { ticket_id: result.ticket_id }) };
|
|
161
|
+
}
|
|
162
|
+
/** Pauses for a person: the card goes to Review with the question as a checkpoint comment. */
|
|
163
|
+
export async function checkpointTicket(c, input) {
|
|
164
|
+
const ticketId = requiredString(input.ticket_id, "ticket_id");
|
|
165
|
+
await c.data.write(async (w) => {
|
|
166
|
+
const ticket = await ticketOn(w, ticketId);
|
|
167
|
+
const lanes = await Lanes.of(w);
|
|
168
|
+
const review = lanes.forRole("review", "Review");
|
|
169
|
+
if (!review)
|
|
170
|
+
throw new OrbitError("missing_review_state");
|
|
171
|
+
const message = requiredString(input.message, "message");
|
|
172
|
+
const at = c.clock.now();
|
|
173
|
+
await w.tickets.update(ticket.id, { state_id: review.id, updated_at: at });
|
|
174
|
+
await noteComment(w, c, ticket.id, "checkpoint", message, at);
|
|
175
|
+
await record(w, c, "checkpoint_requested", ticket.id, { from: lanes.nameOf(ticket), state: review.name, to: review.name, actor_type: c.actor.type, actor_id: c.actor.id });
|
|
176
|
+
await reindex(w, ticket.id);
|
|
177
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
178
|
+
});
|
|
179
|
+
c.changed(c.board.id);
|
|
180
|
+
return contextPack(c, ticketId);
|
|
181
|
+
}
|
|
182
|
+
/** Hands finished work to a person: the card goes to Review (or the lane named) with the summary saved. */
|
|
183
|
+
export async function completeTicket(c, input) {
|
|
184
|
+
const ticketId = requiredString(input.ticket_id, "ticket_id");
|
|
185
|
+
await c.data.write(async (w) => {
|
|
186
|
+
const ticket = await ticketOn(w, ticketId);
|
|
187
|
+
const lanes = await Lanes.of(w);
|
|
188
|
+
const next = input.next_state ? lanes.byName(asText(input.next_state)) : lanes.forRole("review", "Review");
|
|
189
|
+
if (!next)
|
|
190
|
+
throw new OrbitError("missing_next_state");
|
|
191
|
+
const summary = requiredString(input.summary, "summary");
|
|
192
|
+
const prUrl = typeof input.pr_url === "string" ? input.pr_url : "";
|
|
193
|
+
const at = c.clock.now();
|
|
194
|
+
await noteComment(w, c, ticket.id, "completion", [summary, ...(prUrl ? [`PR: ${prUrl}`] : [])].join("\n\n"), at);
|
|
195
|
+
const given = input.implementation_artifact_path || input.summary_artifact_path;
|
|
196
|
+
const artifact = given ? cardLinkFromInput(c, c.board.id, given) : ticket.implementation_artifact_path;
|
|
197
|
+
await w.tickets.update(ticket.id, {
|
|
198
|
+
state_id: next.id,
|
|
199
|
+
implementation_summary: summary,
|
|
200
|
+
implementation_artifact_path: artifact,
|
|
201
|
+
implementation_updates: input.updates ? appendNote(ticket.implementation_updates, input.updates) : ticket.implementation_updates,
|
|
202
|
+
updated_at: at
|
|
203
|
+
});
|
|
204
|
+
await record(w, c, "agent_completed", ticket.id, { from: lanes.nameOf(ticket), next_state: next.name, to: next.name, pr_url: prUrl, actor_type: c.actor.type, actor_id: c.actor.id });
|
|
205
|
+
await reindex(w, ticket.id);
|
|
206
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
207
|
+
});
|
|
208
|
+
c.changed(c.board.id);
|
|
209
|
+
return contextPack(c, ticketId);
|
|
210
|
+
}
|
|
211
|
+
const LONG_FIELDS = ["description", "ai_plan", "implementation_summary", "implementation_updates"];
|
|
212
|
+
function capFields(ticket, max) {
|
|
213
|
+
if (!max)
|
|
214
|
+
return ticket;
|
|
215
|
+
const copy = { ...ticket };
|
|
216
|
+
for (const field of LONG_FIELDS)
|
|
217
|
+
if (Object.hasOwn(copy, field))
|
|
218
|
+
copy[field] = cap(copy[field], max);
|
|
219
|
+
return copy;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A card with what is around it: its board, epic, sub-cards (with their
|
|
223
|
+
* labels and blockers), relations, blockers and related cards. `full` adds
|
|
224
|
+
* the board's rules and journal.
|
|
225
|
+
*/
|
|
226
|
+
export async function contextPack(c, ticketId, options = {}, full = false) {
|
|
227
|
+
const max = positiveInt(options.max_chars_per_field, 0);
|
|
228
|
+
const parentFull = Boolean(options.include_parent_full);
|
|
229
|
+
const relatedFull = Boolean(options.include_related_full);
|
|
230
|
+
const pack = await c.data.read(async (r) => {
|
|
231
|
+
const ticket = await ticketOn(r, ticketId);
|
|
232
|
+
const [lanes, board] = await Promise.all([Lanes.of(r), r.board.get()]);
|
|
233
|
+
const view = (t) => ticketView(t, lanes, c.board);
|
|
234
|
+
const relations = await relationRows(r, ticketId, lanes, c.board);
|
|
235
|
+
const blockers = await unresolvedBlockers(r, ticketId, lanes);
|
|
236
|
+
const parent = ticket.parent_ticket_id ? await r.tickets.get(ticket.parent_ticket_id) : null;
|
|
237
|
+
const children = await r.tickets.list({ parent: ticketId, archived: false });
|
|
238
|
+
const grandchildren = children.length ? await r.tickets.list({ archived: false }) : [];
|
|
239
|
+
const related = new Map();
|
|
240
|
+
for (const t of [...relations.map((row) => row.other_ticket), ...children.map(view), ...(parent ? [view(parent)] : [])])
|
|
241
|
+
if (!related.has(t.id))
|
|
242
|
+
related.set(t.id, t);
|
|
243
|
+
const labels = await labelsByTicket(r, [ticketId, ...children.map((t) => t.id), ...(parent ? [parent.id] : []), ...related.keys()]);
|
|
244
|
+
const labelsOf = (id) => labels.get(id) ?? [];
|
|
245
|
+
const childRows = [];
|
|
246
|
+
for (const child of children) {
|
|
247
|
+
childRows.push({
|
|
248
|
+
...capFields({ ...view(child), labels: labelsOf(child.id), blockers: await unresolvedBlockers(r, child.id, lanes) }, max),
|
|
249
|
+
child_count: grandchildren.filter((t) => t.parent_ticket_id === child.id).length
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const pack = {
|
|
253
|
+
ticket: capFields({ ...view(ticket), labels: labelsOf(ticketId) }, max),
|
|
254
|
+
board: { id: board.id, slug: board.slug, name: board.name, default_branch: board.default_branch || "main" },
|
|
255
|
+
parent_ticket: parent ? (parentFull ? capFields({ ...view(parent), labels: labelsOf(parent.id) }, max) : compactTicket(view(parent))) : null,
|
|
256
|
+
child_tickets: childRows,
|
|
257
|
+
relations: relations.map((row) => ({ ...row, other_ticket: relatedFull ? capFields({ ...row.other_ticket, labels: labelsOf(row.other_ticket.id) }, max) : compactTicket(row.other_ticket) })),
|
|
258
|
+
blockers,
|
|
259
|
+
related_tickets: [...related.values()].map((t) => (relatedFull ? capFields({ ...t, labels: labelsOf(t.id) }, max) : compactTicket(t)))
|
|
260
|
+
};
|
|
261
|
+
const elsewhere = await crossBlockersFor(c, r, ticketId);
|
|
262
|
+
if (elsewhere.length)
|
|
263
|
+
pack.waiting_on = elsewhere;
|
|
264
|
+
if (full)
|
|
265
|
+
pack.board_manual = manualOf(c, board, await r.entries.list());
|
|
266
|
+
return pack;
|
|
267
|
+
});
|
|
268
|
+
return pack;
|
|
269
|
+
}
|
|
270
|
+
function acceptance(description) {
|
|
271
|
+
const match = /(?:^|\n)\s*(?:acceptance criteria|acceptance)\s*:?\s*\n([\s\S]*)/i.exec(description);
|
|
272
|
+
return match?.[1]?.trim() ?? "";
|
|
273
|
+
}
|
|
274
|
+
/** A lean packet for handing one card to an agent: capped text, blockers, the lanes it will move through, recent visible comments. */
|
|
275
|
+
export async function dispatchPacket(c, ticketId, options = {}) {
|
|
276
|
+
const max = positiveInt(options.max_chars_per_field, 4000);
|
|
277
|
+
const commentLimit = Math.min(nonNegativeInt(options.comment_limit, 5), 20);
|
|
278
|
+
return c.data.read(async (r) => {
|
|
279
|
+
const ticket = await ticketOn(r, ticketId);
|
|
280
|
+
const [lanes, board, labels, comments] = await Promise.all([Lanes.of(r), r.board.get(), labelsByTicket(r, [ticketId]), r.comments.list(ticketId)]);
|
|
281
|
+
const parent = ticket.parent_ticket_id ? await r.tickets.get(ticket.parent_ticket_id) : null;
|
|
282
|
+
const blockers = [...(await unresolvedBlockers(r, ticketId, lanes)).map(compactTicket), ...(await crossBlockersFor(c, r, ticketId)).map(compactCross)];
|
|
283
|
+
const visible = comments.filter((comment) => !comment.omitted_for_ai);
|
|
284
|
+
const recent = (commentLimit ? visible.slice(-commentLimit) : [])
|
|
285
|
+
.map((comment) => ({ id: comment.id, author: comment.author, kind: comment.kind, body: cap(comment.body, max), created_at: comment.created_at }));
|
|
286
|
+
const id = (role, name) => lanes.forRole(role, name)?.id ?? null;
|
|
287
|
+
return {
|
|
288
|
+
board: { id: board.id, slug: board.slug, name: board.name, default_branch: board.default_branch || "main", agent_instructions: cap(board.agent_instructions, max) },
|
|
289
|
+
ticket: {
|
|
290
|
+
id: ticket.id,
|
|
291
|
+
ticket_id: ticket.id,
|
|
292
|
+
number: ticket.number,
|
|
293
|
+
title: ticket.title,
|
|
294
|
+
type: ticket.type,
|
|
295
|
+
priority: ticket.priority,
|
|
296
|
+
state_id: ticket.state_id,
|
|
297
|
+
state_name: lanes.nameOf(ticket),
|
|
298
|
+
state_role: lanes.roleOf(ticket),
|
|
299
|
+
description: cap(ticket.description, max),
|
|
300
|
+
acceptance: cap(acceptance(ticket.description), max),
|
|
301
|
+
ai_plan: cap(ticket.ai_plan, max),
|
|
302
|
+
plan_artifact_path: ticket.plan_artifact_path,
|
|
303
|
+
implementation_summary: cap(ticket.implementation_summary, max),
|
|
304
|
+
implementation_artifact_path: ticket.implementation_artifact_path,
|
|
305
|
+
implementation_updates: cap(ticket.implementation_updates, max),
|
|
306
|
+
labels: (labels.get(ticketId) ?? []).map((label) => label.name)
|
|
307
|
+
},
|
|
308
|
+
blockers: { can_start: blockers.length === 0, blockers },
|
|
309
|
+
parent_ticket: parent ? compactTicket({ ...laned(parent, lanes), board_slug: c.board.slug }) : null,
|
|
310
|
+
relevant_state_ids: {
|
|
311
|
+
current: ticket.state_id,
|
|
312
|
+
current_role: lanes.roleOf(ticket),
|
|
313
|
+
ai_ready: id("ai_ready", "AI Ready"),
|
|
314
|
+
in_progress: id("in_progress", "In Progress"),
|
|
315
|
+
review: id("review", "Review"),
|
|
316
|
+
done: id("done", "Done"),
|
|
317
|
+
todo: lanes.byName("Todo")?.id ?? null
|
|
318
|
+
},
|
|
319
|
+
recent_comments: recent
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
export { boardManual };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boards: making one with its lanes, its settings, its journal, its context
|
|
3
|
+
* for agents, and deleting it.
|
|
4
|
+
*/
|
|
5
|
+
import type { BoardCtx, Ctx } from "./context.ts";
|
|
6
|
+
import type { Board, BoardEntry, Id, RegisteredBoard } from "./model.ts";
|
|
7
|
+
import type { TicketView } from "./views.ts";
|
|
8
|
+
import type { BoardWriter } from "../store/types.ts";
|
|
9
|
+
export interface NewBoardInput {
|
|
10
|
+
name?: unknown;
|
|
11
|
+
slug?: unknown;
|
|
12
|
+
repo_url?: unknown;
|
|
13
|
+
default_branch?: unknown;
|
|
14
|
+
ai_enabled?: unknown;
|
|
15
|
+
/** The lanes to start with, as [name, role] pairs; the usual seven (six without AI) when absent. */
|
|
16
|
+
states?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** Makes a board with its lanes, and lists it. Answers the board's own record. */
|
|
19
|
+
export declare function createBoard(c: Ctx, input: NewBoardInput): Promise<Board>;
|
|
20
|
+
/**
|
|
21
|
+
* Makes sure the lanes the agent flow needs exist: AI Ready right after Todo
|
|
22
|
+
* (so taking work has a predictable lane), In Progress and Review. Changes
|
|
23
|
+
* only what isn't so.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ensureAgentLanes(w: BoardWriter, c: Ctx, boardId: Id): Promise<void>;
|
|
26
|
+
export interface BoardUpdate {
|
|
27
|
+
name?: unknown;
|
|
28
|
+
repo_url?: unknown;
|
|
29
|
+
default_branch?: unknown;
|
|
30
|
+
project_notes?: unknown;
|
|
31
|
+
agent_instructions?: unknown;
|
|
32
|
+
ai_enabled?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/** Changes a board's settings; turning AI on adds the agent lanes. */
|
|
35
|
+
export declare function updateBoard(c: BoardCtx, input: BoardUpdate): Promise<Board>;
|
|
36
|
+
export interface BoardManual {
|
|
37
|
+
board: Omit<Board, "project_notes">;
|
|
38
|
+
entries: BoardEntry[];
|
|
39
|
+
/** Where this Orbit runs, for the person's own app; never in an agent's answer. */
|
|
40
|
+
deployment?: {
|
|
41
|
+
app_root: string;
|
|
42
|
+
data_dir: string;
|
|
43
|
+
data_dir_default: boolean;
|
|
44
|
+
mcp_server_path: string;
|
|
45
|
+
db_path: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The board's rules for agents and its journal (newest first; struck entries only when asked). */
|
|
49
|
+
export declare function boardManual(c: BoardCtx, options?: {
|
|
50
|
+
includeStruck?: boolean;
|
|
51
|
+
}): Promise<BoardManual>;
|
|
52
|
+
export declare function manualOf(c: BoardCtx, board: Board, entries: BoardEntry[], options?: {
|
|
53
|
+
includeStruck?: boolean;
|
|
54
|
+
}): BoardManual;
|
|
55
|
+
export interface NewEntryInput {
|
|
56
|
+
type?: unknown;
|
|
57
|
+
title?: unknown;
|
|
58
|
+
body?: unknown;
|
|
59
|
+
ticket_id?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** A decision or a lesson in the board's journal, optionally about a card. */
|
|
62
|
+
export declare function createEntry(c: BoardCtx, input: NewEntryInput): Promise<BoardEntry>;
|
|
63
|
+
/** Strikes a journal entry, or brings it back. */
|
|
64
|
+
export declare function strikeEntry(c: BoardCtx, entryId: Id, input: {
|
|
65
|
+
struck?: unknown;
|
|
66
|
+
}): Promise<BoardEntry>;
|
|
67
|
+
/**
|
|
68
|
+
* Deletes a struck journal entry for good. Only a person may: an agent strikes
|
|
69
|
+
* an entry, which takes it out of agents' context and leaves the person to
|
|
70
|
+
* decide. An entry still live is refused, so nothing goes in one step.
|
|
71
|
+
*/
|
|
72
|
+
export declare function deleteEntry(c: BoardCtx, entryId: Id): Promise<{
|
|
73
|
+
ok: true;
|
|
74
|
+
deleted_id: Id;
|
|
75
|
+
}>;
|
|
76
|
+
/** A board's archived cards, most recently archived first. */
|
|
77
|
+
export declare function archivedTickets(c: BoardCtx): Promise<TicketView[]>;
|
|
78
|
+
export interface BoardListing {
|
|
79
|
+
id: Id;
|
|
80
|
+
slug: string;
|
|
81
|
+
name: string;
|
|
82
|
+
repo_url: string;
|
|
83
|
+
default_branch: string;
|
|
84
|
+
/** Where the board's data is, for the person's own app; never for an agent. */
|
|
85
|
+
db_path?: string;
|
|
86
|
+
last_active_at: string | null;
|
|
87
|
+
}
|
|
88
|
+
/** Every board, by name: a small list without opening any board. */
|
|
89
|
+
export declare function listBoards(c: Ctx): Promise<{
|
|
90
|
+
boards: BoardListing[];
|
|
91
|
+
}>;
|
|
92
|
+
export interface RemovedBoard {
|
|
93
|
+
ok: true;
|
|
94
|
+
deleted_board_id: Id;
|
|
95
|
+
deleted_board_slug: string;
|
|
96
|
+
deleted_files: boolean;
|
|
97
|
+
/** What was deleted, for the person's own app; never for an agent. */
|
|
98
|
+
artifacts?: {
|
|
99
|
+
path: string;
|
|
100
|
+
removed: true;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Deletes a board once its slug is typed back. A copy is kept first where the
|
|
105
|
+
* storage can make one. With `delete_files: false` the board leaves the list
|
|
106
|
+
* and its data stays. Never an agent's, as with a replace: refused before
|
|
107
|
+
* anything is read, whatever the transport (the JSON API, or a host's
|
|
108
|
+
* in-process client).
|
|
109
|
+
*/
|
|
110
|
+
export declare function deleteBoard(c: Ctx, board: RegisteredBoard, input: {
|
|
111
|
+
confirm_slug?: unknown;
|
|
112
|
+
delete_files?: unknown;
|
|
113
|
+
}): Promise<RemovedBoard>;
|