@rse/ase 1.0.7 → 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.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -0,0 +1,538 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import Hapi from "@hapi/hapi";
7
+ import { WebSocketServer } from "ws";
8
+ import * as Core from "./ase-task-store-core.js";
9
+ import * as TaskFormat from "./ase-task-format.js";
10
+ import { AgentRelayBinding } from "./ase-agent-relay-server-bind.js";
11
+ import { ServerGuard, urlHost } from "./ase-lib-server-bind.js";
12
+ /* the route path templates and the JSON payload route options */
13
+ const P = "/projects/{prjId}";
14
+ const T = `${P}/tasks/{taskId}`;
15
+ const json = { payload: { parse: true, allow: "application/json" } };
16
+ /* the interval of the keepalive pings of the subscribers, the maximum size of the
17
+ messages of the subscribers (which send none at all), the maximum amount of not
18
+ yet transmitted data of a subscriber (beyond which it cannot keep up, generously
19
+ bounded as the frames carry entire task plans), and the maximum number of
20
+ subscribers per project and in total */
21
+ const PING_MS = 15 * 1000;
22
+ const PAYLOAD_MAX = 4 * 1024;
23
+ const BUFFER_MAX = 16 * 1024 * 1024;
24
+ const SUBS_MAX = 128;
25
+ const SUBS_TOTAL_MAX = 1024;
26
+ /* the HTTP binding of the REST API: bearer token authentication, CORS,
27
+ the routes onto the core operations, problem details rendering, and
28
+ the WebSocket event endpoint, optionally co-hosting the agent relay */
29
+ export class TaskStoreServer {
30
+ log;
31
+ options;
32
+ server;
33
+ wss;
34
+ core;
35
+ subs = new Map();
36
+ guard;
37
+ relay;
38
+ unanswered = new WeakSet();
39
+ timer = null;
40
+ constructor(log, options) {
41
+ this.log = log;
42
+ this.options = options;
43
+ this.relay = options.relay ? new AgentRelayBinding(log) : null;
44
+ this.core = new Core.TaskStoreCore(options.store, (prjId, frame) => this.emit(prjId, frame));
45
+ this.server = Hapi.server({
46
+ host: options.address,
47
+ port: options.port,
48
+ tls: options.tls ?? undefined,
49
+ routes: {
50
+ cors: options.cors.length === 0 ? false : {
51
+ origin: options.cors,
52
+ headers: ["Authorization", "Content-Type", "If-None-Match", "If-Match"],
53
+ exposedHeaders: ["Location", "ETag"],
54
+ maxAge: 86400
55
+ }
56
+ }
57
+ });
58
+ this.wss = new WebSocketServer({ noServer: true, maxPayload: PAYLOAD_MAX });
59
+ /* install the common extensions, logging the requests of the co-hosted
60
+ agent relay at debug level only (as the agents announce frequently) */
61
+ this.guard = new ServerGuard(log, {
62
+ label: "task store",
63
+ token: options.token,
64
+ failure: "internal task store failure (see service log for details)",
65
+ level: (path) => this.relay !== null && AgentRelayBinding.handles(path) ? "debug" : "info",
66
+ upgrade: (req, socket, head) => this.upgrade(req, socket, head)
67
+ });
68
+ this.guard.install(this.server);
69
+ this.installRoutes();
70
+ }
71
+ /* start the server: open the storage, start the co-hosted agent relay,
72
+ bind the listener, and start the keepalive pings of the subscribers */
73
+ async start() {
74
+ await this.options.store.open();
75
+ this.relay?.start();
76
+ try {
77
+ await this.server.start();
78
+ }
79
+ catch (err) {
80
+ /* ignore a close failure to not mask the original start failure */
81
+ this.relay?.stop();
82
+ await this.options.store.close().catch(() => { });
83
+ throw err;
84
+ }
85
+ this.timer = setInterval(() => { this.keepalive(); }, PING_MS);
86
+ const scheme = this.options.tls !== null ? "https" : "http";
87
+ this.log.write("info", `task store: listening on ${scheme}://${urlHost(this.options.address)}:${this.options.port} ` +
88
+ `(storage plugin "${this.options.store.name}"${this.relay !== null ? ", with agent relay" : ""})`);
89
+ }
90
+ /* stop the server: stop the keepalive pings, close all subscriptions with close code
91
+ 1001, stop the co-hosted agent relay, unbind the listener, and close the storage */
92
+ async stop() {
93
+ if (this.timer !== null)
94
+ clearInterval(this.timer);
95
+ this.timer = null;
96
+ for (const subs of this.subs.values())
97
+ for (const sub of subs)
98
+ sub.ws.close(1001, "server shutting down");
99
+ this.subs.clear();
100
+ this.relay?.stop();
101
+ this.wss.close();
102
+ await this.server.stop({ timeout: 1000 });
103
+ await this.options.store.close();
104
+ this.log.write("info", "task store: stopped");
105
+ }
106
+ /* read the path parameters and a single-valued query parameter */
107
+ params(request) {
108
+ return request.params;
109
+ }
110
+ query(request, name) {
111
+ const value = request.query[name];
112
+ if (Array.isArray(value))
113
+ return value[value.length - 1];
114
+ return value;
115
+ }
116
+ /* the lifecycle model, task id scheme, and sequence number
117
+ high-water mark as exposed by the project endpoints */
118
+ projectView(project) {
119
+ return {
120
+ id: project.id,
121
+ lifecycle: {
122
+ name: project.lifecycle.name,
123
+ states: project.lifecycle.states,
124
+ initial: project.lifecycle.initial,
125
+ finished: project.lifecycle.finished,
126
+ transitions: project.lifecycle.transitions
127
+ },
128
+ idscheme: project.idscheme,
129
+ seqmark: project.seqmark
130
+ };
131
+ }
132
+ /* strip the "created" flag of a core result and map it onto the status code */
133
+ created(h, result) {
134
+ const { created, ...body } = result;
135
+ return h.response(body).code(created ? 201 : 200);
136
+ }
137
+ /* ==== events ==== */
138
+ /* ping all subscribers, terminating those which have not answered the previous
139
+ ping (e.g. on a half-open connection after a network partition or a host crash) */
140
+ keepalive() {
141
+ for (const [prjId, subs] of this.subs)
142
+ for (const sub of subs) {
143
+ if (this.unanswered.has(sub.ws)) {
144
+ this.log.write("debug", `task store: terminating unresponsive subscriber of project "${prjId}"`);
145
+ sub.ws.terminate();
146
+ continue;
147
+ }
148
+ this.unanswered.add(sub.ws);
149
+ sub.ws.ping();
150
+ }
151
+ }
152
+ /* deliver an event frame to the subscribers of a project, each
153
+ restricted to the task ids it subscribed to (except for a
154
+ lifecycle model or task id scheme change, which concerns all task plans),
155
+ terminating the subscribers which cannot keep up (as they would miss
156
+ events, and are notified of a change on their reconnection anyway) */
157
+ emit(prjId, frame) {
158
+ const subs = this.subs.get(prjId);
159
+ if (subs === undefined)
160
+ return;
161
+ for (const sub of subs) {
162
+ if (sub.ws.readyState !== sub.ws.OPEN)
163
+ continue;
164
+ if (sub.ws.bufferedAmount > BUFFER_MAX) {
165
+ this.log.write("debug", `task store: terminating congested subscriber of project "${prjId}"`);
166
+ sub.ws.terminate();
167
+ continue;
168
+ }
169
+ const out = {};
170
+ const keep = (id) => sub.tasks === null || sub.tasks.has(id);
171
+ if (frame.added !== undefined) {
172
+ const added = Object.fromEntries(Object.entries(frame.added).filter(([id]) => keep(id)));
173
+ if (Object.keys(added).length > 0)
174
+ out.added = added;
175
+ }
176
+ if (frame.updated !== undefined) {
177
+ const updated = Object.fromEntries(Object.entries(frame.updated).filter(([id]) => keep(id)));
178
+ if (Object.keys(updated).length > 0)
179
+ out.updated = updated;
180
+ }
181
+ if (frame.deleted !== undefined) {
182
+ const deleted = frame.deleted.filter((id) => keep(id));
183
+ if (deleted.length > 0)
184
+ out.deleted = deleted;
185
+ }
186
+ if (frame.lifecycle !== undefined)
187
+ out.lifecycle = frame.lifecycle;
188
+ if (frame.idscheme !== undefined)
189
+ out.idscheme = frame.idscheme;
190
+ if (Object.keys(out).length > 0)
191
+ sub.ws.send(JSON.stringify(out));
192
+ }
193
+ }
194
+ /* handle a WebSocket handshake on the event endpoint (or, after its
195
+ authentication, on the event endpoint of the co-hosted agent relay) */
196
+ async upgrade(req, socket, head) {
197
+ const url = new URL(req.url ?? "/", "http://localhost");
198
+ const path = url.pathname;
199
+ if (this.relay !== null && AgentRelayBinding.handles(path)) {
200
+ if (!this.guard.authorized(req.headers.authorization, url.searchParams.get("token") ?? undefined))
201
+ this.guard.rejectUpgrade(socket, 401, "missing or invalid bearer token", path);
202
+ else
203
+ this.relay.upgrade(req, socket, head, path, (status, detail) => {
204
+ this.guard.rejectUpgrade(socket, status, detail, path);
205
+ });
206
+ return;
207
+ }
208
+ const m = /^\/projects\/([^/]+)\/events$/.exec(path);
209
+ if (m === null) {
210
+ this.guard.rejectUpgrade(socket, 404, `no WebSocket endpoint "${path}"`, path);
211
+ return;
212
+ }
213
+ if (!this.guard.authorized(req.headers.authorization, url.searchParams.get("token") ?? undefined)) {
214
+ this.guard.rejectUpgrade(socket, 401, "missing or invalid bearer token", path);
215
+ return;
216
+ }
217
+ const prjId = m[1];
218
+ if (!TaskFormat.ID_RE.test(prjId)) {
219
+ this.guard.rejectUpgrade(socket, 422, `invalid project id "${prjId}" (expected: [A-Za-z0-9_-]+)`, path);
220
+ return;
221
+ }
222
+ if (!await this.core.projectExists(prjId)) {
223
+ this.guard.rejectUpgrade(socket, 404, `no project "${prjId}" registered`, path);
224
+ return;
225
+ }
226
+ let tasks = null;
227
+ const list = url.searchParams.get("tasks");
228
+ if (list !== null) {
229
+ tasks = new Set();
230
+ for (const id of list.split(",").map((token) => token.trim()).filter((token) => token !== "")) {
231
+ if (!TaskFormat.TASK_ID_RE.test(id)) {
232
+ this.guard.rejectUpgrade(socket, 422, `invalid task id "${id}" (expected: [A-Za-z0-9#][A-Za-z0-9#_-]*)`, path);
233
+ return;
234
+ }
235
+ tasks.add(id);
236
+ }
237
+ }
238
+ /* reject a subscriber beyond the maximum numbers */
239
+ let total = 0;
240
+ for (const subs of this.subs.values())
241
+ total += subs.size;
242
+ if ((this.subs.get(prjId)?.size ?? 0) >= SUBS_MAX) {
243
+ this.guard.rejectUpgrade(socket, 429, `too many subscribers of project "${prjId}" (maximum: ${SUBS_MAX})`, path);
244
+ return;
245
+ }
246
+ if (total >= SUBS_TOTAL_MAX) {
247
+ this.guard.rejectUpgrade(socket, 429, `too many subscribers in total (maximum: ${SUBS_TOTAL_MAX})`, path);
248
+ return;
249
+ }
250
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
251
+ const sub = { ws, tasks };
252
+ let subs = this.subs.get(prjId);
253
+ if (subs === undefined) {
254
+ subs = new Set();
255
+ this.subs.set(prjId, subs);
256
+ }
257
+ subs.add(sub);
258
+ this.log.write("debug", `task store: subscribed to events of project "${prjId}"` +
259
+ (tasks !== null ? ` (tasks: ${[...tasks].join(", ")})` : ""));
260
+ ws.on("pong", () => {
261
+ this.unanswered.delete(ws);
262
+ });
263
+ ws.on("close", () => {
264
+ const subs = this.subs.get(prjId);
265
+ if (subs !== undefined) {
266
+ subs.delete(sub);
267
+ if (subs.size === 0)
268
+ this.subs.delete(prjId);
269
+ }
270
+ });
271
+ ws.on("error", (err) => {
272
+ this.log.write("debug", `task store: WebSocket error on events of project "${prjId}": ${err.message}`);
273
+ });
274
+ });
275
+ }
276
+ /* ==== routes ==== */
277
+ /* install the routes onto the core operations (and of the co-hosted agent relay) */
278
+ installRoutes() {
279
+ this.installProjectRoutes();
280
+ this.installTaskRoutes();
281
+ this.installPartRoutes();
282
+ this.installAttachmentRoutes();
283
+ this.relay?.install(this.server);
284
+ }
285
+ /* install the project endpoints (and the plain HTTP fallback of the event endpoint) */
286
+ installProjectRoutes() {
287
+ const core = this.core;
288
+ const p = (request) => this.params(request);
289
+ /* the project endpoints */
290
+ this.server.route({
291
+ method: "GET",
292
+ path: "/projects",
293
+ handler: async () => ({ projects: await core.projectList() })
294
+ });
295
+ this.server.route({
296
+ method: "GET",
297
+ path: P,
298
+ handler: async (request) => this.projectView(await core.projectGet(p(request).prjId))
299
+ });
300
+ this.server.route({
301
+ method: "PUT",
302
+ path: P,
303
+ options: json,
304
+ handler: async (request, h) => {
305
+ const payload = request.payload;
306
+ if (payload !== null && payload !== undefined && !Core.isObject(payload))
307
+ throw Core.problem(400, "request body has to be an object");
308
+ /* "If-None-Match: *" registers the project only if not yet registered */
309
+ const createOnly = request.headers["if-none-match"] === "*";
310
+ const result = await core.projectSet(p(request).prjId, Core.isObject(payload) ?
311
+ { lifecycle: payload.lifecycle, idscheme: payload.idscheme } : {}, createOnly);
312
+ return h.response(this.projectView(result.project)).code(result.created ? 201 : 200);
313
+ }
314
+ });
315
+ this.server.route({
316
+ method: "DELETE",
317
+ path: P,
318
+ handler: async (request, h) => {
319
+ const prjId = p(request).prjId;
320
+ await core.projectDelete(prjId);
321
+ const subs = this.subs.get(prjId);
322
+ if (subs !== undefined) {
323
+ for (const sub of subs)
324
+ sub.ws.close(1001, "project unregistered");
325
+ this.subs.delete(prjId);
326
+ }
327
+ return h.response().code(204);
328
+ }
329
+ });
330
+ /* a plain HTTP request on the event endpoint (the WebSocket
331
+ handshakes are intercepted on the listener's "upgrade" event) */
332
+ this.server.route({
333
+ method: "GET",
334
+ path: `${P}/events`,
335
+ handler: async (request) => {
336
+ await core.projectGet(p(request).prjId);
337
+ throw Core.problem(426, "the event endpoint requires a WebSocket handshake");
338
+ }
339
+ });
340
+ }
341
+ /* install the task plan endpoints */
342
+ installTaskRoutes() {
343
+ const core = this.core;
344
+ const p = (request) => this.params(request);
345
+ /* the task plan endpoints */
346
+ this.server.route({
347
+ method: "GET",
348
+ path: `${P}/tasks`,
349
+ handler: async (request) => ({
350
+ tasks: await core.taskList(p(request).prjId, this.query(request, "include"), this.query(request, "exclude"), this.query(request, "fields"))
351
+ })
352
+ });
353
+ this.server.route({
354
+ method: "DELETE",
355
+ path: `${P}/tasks`,
356
+ handler: async (request) => ({ purged: await core.taskPurge(p(request).prjId, this.query(request, "age")) })
357
+ });
358
+ this.server.route({
359
+ method: "POST",
360
+ path: `${P}/newid`,
361
+ options: json,
362
+ handler: async (request) => {
363
+ const payload = request.payload;
364
+ if (payload !== null && payload !== undefined && !Core.isObject(payload))
365
+ throw Core.problem(400, "request body has to be an object");
366
+ return core.taskNewId(p(request).prjId, Core.isObject(payload) ? payload : {});
367
+ }
368
+ });
369
+ this.server.route({
370
+ method: "GET",
371
+ path: T,
372
+ handler: async (request, h) => {
373
+ const plan = await core.taskLoad(p(request).prjId, p(request).taskId);
374
+ return h.response(plan).etag(Core.taskTag(plan));
375
+ }
376
+ });
377
+ this.server.route({
378
+ method: "PUT",
379
+ path: T,
380
+ options: json,
381
+ handler: async (request, h) => {
382
+ /* "If-Match: <tag>" saves only if the plan is still the one with this entity tag
383
+ (ignoring the content encoding suffix hapi appends to the tag of a compressed response),
384
+ and "If-None-Match: *" saves only if the plan does not exist yet */
385
+ const tag = request.headers["if-match"];
386
+ return this.created(h, await core.taskSave(p(request).prjId, p(request).taskId, request.payload, typeof tag === "string" ? tag.replace(/^(?:W\/)?"(.*?)(?:-(?:gzip|deflate))?"$/, "$1") : undefined, request.headers["if-none-match"] === "*"));
387
+ }
388
+ });
389
+ this.server.route({
390
+ method: "PATCH",
391
+ path: T,
392
+ options: json,
393
+ handler: async (request) => core.taskPatch(p(request).prjId, p(request).taskId, request.payload)
394
+ });
395
+ this.server.route({
396
+ method: "DELETE",
397
+ path: T,
398
+ handler: async (request, h) => {
399
+ await core.taskDelete(p(request).prjId, p(request).taskId);
400
+ return h.response().code(204);
401
+ }
402
+ });
403
+ }
404
+ /* install the header and body part endpoints */
405
+ installPartRoutes() {
406
+ const core = this.core;
407
+ const p = (request) => this.params(request);
408
+ /* the header part endpoints */
409
+ this.server.route({
410
+ method: "GET",
411
+ path: `${T}/header`,
412
+ handler: async (request) => core.headerGet(p(request).prjId, p(request).taskId)
413
+ });
414
+ this.server.route({
415
+ method: "PUT",
416
+ path: `${T}/header`,
417
+ options: json,
418
+ handler: async (request) => {
419
+ const { created, ...body } = await core.headerSet(p(request).prjId, p(request).taskId, request.payload);
420
+ return body;
421
+ }
422
+ });
423
+ this.server.route({
424
+ method: "GET",
425
+ path: `${T}/header/content`,
426
+ handler: async (request, h) => h.response(await core.headerContent(p(request).prjId, p(request).taskId)).type("text/plain; charset=utf-8")
427
+ });
428
+ this.server.route({
429
+ method: "GET",
430
+ path: `${T}/header/key/{key}`,
431
+ handler: async (request) => ({ value: await core.headerKeyGet(p(request).prjId, p(request).taskId, p(request).key) })
432
+ });
433
+ this.server.route({
434
+ method: "PUT",
435
+ path: `${T}/header/key/{key}`,
436
+ options: json,
437
+ handler: async (request, h) => this.created(h, await core.headerKeySet(p(request).prjId, p(request).taskId, p(request).key, request.payload))
438
+ });
439
+ this.server.route({
440
+ method: "DELETE",
441
+ path: `${T}/header/key/{key}`,
442
+ handler: async (request, h) => {
443
+ await core.headerKeyDelete(p(request).prjId, p(request).taskId, p(request).key);
444
+ return h.response().code(204);
445
+ }
446
+ });
447
+ /* the body part endpoints */
448
+ this.server.route({
449
+ method: "GET",
450
+ path: `${T}/body`,
451
+ handler: async (request) => ({ body: await core.bodyGet(p(request).prjId, p(request).taskId) })
452
+ });
453
+ this.server.route({
454
+ method: "PUT",
455
+ path: `${T}/body`,
456
+ options: json,
457
+ handler: async (request, h) => {
458
+ await core.bodySet(p(request).prjId, p(request).taskId, request.payload);
459
+ return h.response().code(204);
460
+ }
461
+ });
462
+ }
463
+ /* install the attachment part endpoints */
464
+ installAttachmentRoutes() {
465
+ const core = this.core;
466
+ const p = (request) => this.params(request);
467
+ /* the attachment part endpoints */
468
+ this.server.route({
469
+ method: "GET",
470
+ path: `${T}/attachment`,
471
+ handler: async (request) => {
472
+ const type = this.query(request, "type");
473
+ if (type === undefined)
474
+ return core.attachmentList(p(request).prjId, p(request).taskId);
475
+ return { found: await core.attachmentFind(p(request).prjId, p(request).taskId, type) };
476
+ }
477
+ });
478
+ this.server.route({
479
+ method: "POST",
480
+ path: `${T}/attachment`,
481
+ options: json,
482
+ handler: async (request, h) => {
483
+ const append = this.query(request, "append") === "true";
484
+ const { index, created } = await core.attachmentAdd(p(request).prjId, p(request).taskId, request.payload, append);
485
+ return h.response({ index }).code(created ? 201 : 200).header("Location", `${request.path}/${index}`);
486
+ }
487
+ });
488
+ this.server.route({
489
+ method: "GET",
490
+ path: `${T}/attachment/{index}`,
491
+ handler: async (request) => core.attachmentGet(p(request).prjId, p(request).taskId, p(request).index)
492
+ });
493
+ this.server.route({
494
+ method: "PUT",
495
+ path: `${T}/attachment/{index}`,
496
+ options: json,
497
+ handler: async (request, h) => {
498
+ await core.attachmentSet(p(request).prjId, p(request).taskId, p(request).index, request.payload);
499
+ return h.response().code(204);
500
+ }
501
+ });
502
+ this.server.route({
503
+ method: "DELETE",
504
+ path: `${T}/attachment/{index}`,
505
+ handler: async (request, h) => {
506
+ await core.attachmentDelete(p(request).prjId, p(request).taskId, p(request).index);
507
+ return h.response().code(204);
508
+ }
509
+ });
510
+ this.server.route({
511
+ method: "GET",
512
+ path: `${T}/attachment/{index}/content`,
513
+ handler: async (request, h) => {
514
+ const { type, content } = await core.attachmentContent(p(request).prjId, p(request).taskId, p(request).index);
515
+ return h.response(content).type(type);
516
+ }
517
+ });
518
+ this.server.route({
519
+ method: "GET",
520
+ path: `${T}/attachment/{index}/key/{key}`,
521
+ handler: async (request) => ({ value: await core.attachmentKeyGet(p(request).prjId, p(request).taskId, p(request).index, p(request).key) })
522
+ });
523
+ this.server.route({
524
+ method: "PUT",
525
+ path: `${T}/attachment/{index}/key/{key}`,
526
+ options: json,
527
+ handler: async (request, h) => this.created(h, await core.attachmentKeySet(p(request).prjId, p(request).taskId, p(request).index, p(request).key, request.payload))
528
+ });
529
+ this.server.route({
530
+ method: "DELETE",
531
+ path: `${T}/attachment/{index}/key/{key}`,
532
+ handler: async (request, h) => {
533
+ await core.attachmentKeyDelete(p(request).prjId, p(request).taskId, p(request).index, p(request).key);
534
+ return h.response().code(204);
535
+ }
536
+ });
537
+ }
538
+ }