@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,495 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { ofetch } from "ofetch";
9
+ import { Agent } from "undici";
10
+ import { WebSocket } from "ws";
11
+ import { LRUCache } from "lru-cache";
12
+ import { Config } from "./ase-config-core.js";
13
+ import { configSchema } from "./ase-config-schema.js";
14
+ import { parseScope, userStateDir } from "./ase-config-scope.js";
15
+ import * as Delegate from "./ase-task-store-plugin-delegate.js";
16
+ import * as Core from "./ase-task-store-core.js";
17
+ import * as TaskFormat from "./ase-task-format.js";
18
+ /* the local client: the REST API functionality operating in-process
19
+ on a built-in storage plugin (the "ase" one in "solo" mode), with the project
20
+ registered under its configured lifecycle model and task id scheme on every open */
21
+ export class LocalTaskStoreClient {
22
+ prjId;
23
+ lifecycle;
24
+ idscheme;
25
+ storage;
26
+ log;
27
+ /* the storage delegates, shared by all concurrently open clients of the
28
+ same store (keyed by the store only, as the lifecycle model and task
29
+ id scheme are re-registered on every open and hence survive a switch without a
30
+ second delegate), so its per-project queue serializes in-process, too */
31
+ static shared = new Map();
32
+ entry;
33
+ core;
34
+ constructor(prjId, lifecycle, idscheme, storage, log) {
35
+ this.prjId = prjId;
36
+ this.lifecycle = lifecycle;
37
+ this.idscheme = idscheme;
38
+ this.storage = storage;
39
+ this.log = log;
40
+ }
41
+ /* persist a configuration key on scope "project" (which the local store always follows) */
42
+ configure(key, value) {
43
+ const cfg = new Config("config", configSchema, this.log, parseScope("project"));
44
+ cfg.lock(() => {
45
+ cfg.read();
46
+ cfg.set(key, value);
47
+ cfg.write();
48
+ });
49
+ }
50
+ /* switch the lifecycle model by persisting "project.task.lifecycle"
51
+ and re-registering the project (mapping the plan states) */
52
+ async setLifecycle(name) {
53
+ this.configure("project.task.lifecycle", name);
54
+ this.lifecycle = (await this.core.projectSet(this.prjId, { lifecycle: name })).project.lifecycle;
55
+ }
56
+ /* switch the task id scheme by persisting "project.task.idscheme" and re-registering the project */
57
+ async setIdScheme(spec) {
58
+ this.configure("project.task.idscheme", spec);
59
+ this.idscheme = (await this.core.projectSet(this.prjId, { idscheme: spec })).project.idscheme;
60
+ }
61
+ async missing(op, fallback) {
62
+ try {
63
+ return await op();
64
+ }
65
+ catch (err) {
66
+ if (err instanceof Core.Problem && err.status === 404)
67
+ return fallback;
68
+ throw err;
69
+ }
70
+ }
71
+ async open() {
72
+ let entry = LocalTaskStoreClient.shared.get(this.storage.key);
73
+ if (entry === undefined) {
74
+ const listeners = new Set();
75
+ const opened = (async () => {
76
+ const plugin = await Delegate.loadTaskStoragePlugin(this.storage.plugin, {
77
+ options: this.storage.options,
78
+ log: (level, message) => this.log.write(level, `task: store: ${message}`)
79
+ });
80
+ const store = new Delegate.TaskStore(plugin);
81
+ const core = new Core.TaskStoreCore(store, (prjId, frame) => {
82
+ for (const listener of listeners)
83
+ listener(prjId, frame);
84
+ });
85
+ await store.open();
86
+ return { store, core };
87
+ })();
88
+ entry = { refs: 0, listeners, opened };
89
+ LocalTaskStoreClient.shared.set(this.storage.key, entry);
90
+ }
91
+ entry.refs++;
92
+ this.entry = entry;
93
+ try {
94
+ this.core = (await entry.opened).core;
95
+ await this.core.projectSet(this.prjId, { lifecycle: this.lifecycle.name, idscheme: this.idscheme });
96
+ }
97
+ catch (err) {
98
+ await this.close();
99
+ throw err;
100
+ }
101
+ }
102
+ async close() {
103
+ const entry = this.entry;
104
+ if (entry === undefined)
105
+ return;
106
+ this.entry = undefined;
107
+ if (--entry.refs === 0) {
108
+ if (LocalTaskStoreClient.shared.get(this.storage.key) === entry)
109
+ LocalTaskStoreClient.shared.delete(this.storage.key);
110
+ await entry.opened.then((opened) => opened.store.close(), () => { });
111
+ }
112
+ }
113
+ list(fields) {
114
+ return this.core.taskList(this.prjId, "none", "none", fields ?? "none");
115
+ }
116
+ load(id) {
117
+ return this.missing(() => this.core.taskLoad(this.prjId, id), null);
118
+ }
119
+ async save(id, plan, tag, create = false) {
120
+ return (await this.core.taskSave(this.prjId, id, plan, tag, create)).tag;
121
+ }
122
+ newId(title, proposal, taken) {
123
+ return this.core.taskNewId(this.prjId, { title, proposal, taken });
124
+ }
125
+ patch(id, change) {
126
+ return this.missing(() => this.core.taskPatch(this.prjId, id, change), null);
127
+ }
128
+ delete(id) {
129
+ return this.missing(async () => {
130
+ await this.core.taskDelete(this.prjId, id);
131
+ return true;
132
+ }, false);
133
+ }
134
+ purge(age) {
135
+ return this.core.taskPurge(this.prjId, age);
136
+ }
137
+ content(id, index) {
138
+ return this.missing(() => this.core.attachmentContent(this.prjId, id, String(index)), null);
139
+ }
140
+ attach(id, attachment, append = false) {
141
+ return this.missing(async () => {
142
+ await this.core.attachmentAdd(this.prjId, id, attachment, append);
143
+ return true;
144
+ }, false);
145
+ }
146
+ /* subscribe to the change events of the project, i.e. the changes made
147
+ in-process plus the ones detected by the storage plugin itself (e.g.
148
+ by polling a remote storage), keeping the store opened meanwhile and
149
+ reporting it as connected once opened; returns a function to unsubscribe */
150
+ subscribe(onChange, onState) {
151
+ const listener = (prjId) => {
152
+ if (prjId === this.prjId)
153
+ onChange();
154
+ };
155
+ let stopped = false;
156
+ const start = async () => {
157
+ await this.open();
158
+ if (stopped)
159
+ await this.close();
160
+ else {
161
+ this.entry?.listeners.add(listener);
162
+ onState?.(true);
163
+ }
164
+ };
165
+ start().catch((err) => {
166
+ this.log.write("warning", `task: store: subscription failed: ${err instanceof Error ? err.message : String(err)}`);
167
+ });
168
+ return () => {
169
+ stopped = true;
170
+ this.entry?.listeners.delete(listener);
171
+ this.close().catch(() => { });
172
+ };
173
+ }
174
+ }
175
+ /* the interval of the keepalive pings of the event subscription (where a connection without
176
+ any traffic for this interval is pinged and without any traffic for twice this interval
177
+ is terminated) and the timeout of its WebSocket handshake */
178
+ const KEEPALIVE_MS = 15 * 1000;
179
+ const HANDSHAKE_MS = 10 * 1000;
180
+ /* the remote client: the REST API of a task store server, with the
181
+ project registered under its configured lifecycle model and task id
182
+ scheme on first use only (afterwards adopting the lifecycle model and
183
+ task id scheme of the registered project) and every problem details
184
+ response raised as an error; an "insecure" client skips the TLS
185
+ certificate verification */
186
+ export class RemoteTaskStoreClient {
187
+ prjId;
188
+ configured;
189
+ configuredIdScheme;
190
+ log;
191
+ base;
192
+ token;
193
+ insecure;
194
+ /* the effective lifecycle models and task id schemes of the registered projects
195
+ (TTL-bounded, to spare consecutive operations the registration round-trips) */
196
+ static registered = new LRUCache({ max: 16, ttl: 10 * 1000 });
197
+ dispatcher;
198
+ lifecycle;
199
+ idscheme;
200
+ constructor(prjId, configured, configuredIdScheme, log, base, token, insecure) {
201
+ this.prjId = prjId;
202
+ this.configured = configured;
203
+ this.configuredIdScheme = configuredIdScheme;
204
+ this.log = log;
205
+ this.base = base;
206
+ this.token = token;
207
+ this.insecure = insecure;
208
+ this.lifecycle = configured;
209
+ this.idscheme = configuredIdScheme;
210
+ this.dispatcher = insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined;
211
+ }
212
+ /* raise a failed connection as an unreachable store error,
213
+ reporting the innermost cause (like "connect ECONNREFUSED") */
214
+ unreachable(err) {
215
+ let cause = err;
216
+ while (cause instanceof Error && cause.cause instanceof Error)
217
+ cause = cause.cause;
218
+ const reason = cause instanceof Error ? cause.message : String(cause);
219
+ throw new Error(`task: store "${this.base}" unreachable: ${reason}`, { cause: err });
220
+ }
221
+ /* perform a request: a 404 response or a tolerated error response yields
222
+ a null result, any other error response is raised as a problem carrying
223
+ its status, and a failed connection is raised as an unreachable store error */
224
+ async request(method, url, body, headers = {}, tolerated = []) {
225
+ const r = await ofetch.raw(`${this.base}${url}`, {
226
+ method,
227
+ headers: {
228
+ Authorization: `Bearer ${this.token}`,
229
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
230
+ ...headers
231
+ },
232
+ body: body !== undefined ? JSON.stringify(body) : undefined,
233
+ dispatcher: this.dispatcher,
234
+ signal: AbortSignal.timeout(10000),
235
+ ignoreResponseError: true
236
+ }).catch((err) => this.unreachable(err));
237
+ if (r.status === 404 || tolerated.includes(r.status))
238
+ return null;
239
+ if (r.status < 200 || r.status >= 300) {
240
+ const d = r._data;
241
+ throw new Core.Problem(r.status, `store "${this.base}": ${d?.detail ?? d?.title ?? `HTTP ${r.status}`}`);
242
+ }
243
+ return (r._data ?? {});
244
+ }
245
+ get tasks() {
246
+ return `/projects/${this.prjId}/tasks`;
247
+ }
248
+ get key() {
249
+ return `${this.base}/${this.prjId}`;
250
+ }
251
+ /* adopt the lifecycle model and task id scheme of the registered project
252
+ (the configured task id scheme for a server predating task id schemes) */
253
+ adopt(project) {
254
+ if (project === null)
255
+ throw new Core.Problem(404, `store "${this.base}": project "${this.prjId}" not registered`);
256
+ const lifecycle = TaskFormat.taskLifecycles[project.lifecycle.name];
257
+ if (lifecycle === undefined)
258
+ throw new Error(`task: store "${this.base}" uses unknown lifecycle model "${project.lifecycle.name}"`);
259
+ const idscheme = project.idscheme ?? this.configuredIdScheme;
260
+ if (TaskFormat.checkIdScheme(idscheme) !== "")
261
+ throw new Error(`task: store "${this.base}" uses invalid task id scheme "${idscheme}"`);
262
+ this.lifecycle = lifecycle;
263
+ this.idscheme = idscheme;
264
+ RemoteTaskStoreClient.registered.set(this.key, { lifecycle, idscheme });
265
+ }
266
+ /* raise a not registered project (dropping its cached registration) */
267
+ unregistered() {
268
+ RemoteTaskStoreClient.registered.delete(this.key);
269
+ throw new Core.Problem(404, `store "${this.base}": project "${this.prjId}" not registered`);
270
+ }
271
+ /* pass through a task endpoint result, disambiguating a 404 response
272
+ (null) into a missing task or a not registered project */
273
+ async task(result) {
274
+ if (result === null && await this.request("GET", `/projects/${this.prjId}`) === null)
275
+ this.unregistered();
276
+ return result;
277
+ }
278
+ async open() {
279
+ const cached = RemoteTaskStoreClient.registered.get(this.key);
280
+ if (cached !== undefined) {
281
+ this.lifecycle = cached.lifecycle;
282
+ this.idscheme = cached.idscheme;
283
+ return;
284
+ }
285
+ /* register the project only if not yet registered (via "If-None-Match: *"), as the
286
+ lifecycle model and task id scheme of a registered project are shared by all its clients */
287
+ const created = await this.request("PUT", `/projects/${this.prjId}`, { lifecycle: this.configured.name, idscheme: this.configuredIdScheme }, { "If-None-Match": "*" }, [412]);
288
+ this.adopt(created ?? await this.request("GET", `/projects/${this.prjId}`));
289
+ this.mismatch();
290
+ }
291
+ async setLifecycle(name) {
292
+ this.adopt(await this.request("PUT", `/projects/${this.prjId}`, { lifecycle: name }));
293
+ this.mismatch();
294
+ }
295
+ async setIdScheme(spec) {
296
+ this.adopt(await this.request("PUT", `/projects/${this.prjId}`, { idscheme: spec }));
297
+ this.mismatch();
298
+ }
299
+ /* warn about a configured lifecycle model or task id scheme deviating from the
300
+ one of the registered project, once per deviation only (persisted across processes) */
301
+ mismatch() {
302
+ const file = path.join(userStateDir(), "task-lifecycle.json");
303
+ let seen = {};
304
+ try {
305
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
306
+ if (typeof data === "object" && data !== null && !Array.isArray(data))
307
+ seen = data;
308
+ }
309
+ catch {
310
+ /* no (valid) state yet */
311
+ }
312
+ const checks = [{
313
+ key: this.key,
314
+ config: "project.task.lifecycle",
315
+ cmd: "lifecycle",
316
+ configured: this.configured.name,
317
+ effective: this.lifecycle.name
318
+ }, {
319
+ key: `${this.key}#idscheme`,
320
+ config: "project.task.idscheme",
321
+ cmd: "idscheme",
322
+ configured: this.configuredIdScheme,
323
+ effective: this.idscheme
324
+ }];
325
+ let changed = false;
326
+ for (const check of checks) {
327
+ const pair = check.effective !== check.configured ?
328
+ `${check.configured}:${check.effective}` : undefined;
329
+ if (seen[check.key] === pair)
330
+ continue;
331
+ changed = true;
332
+ if (pair === undefined)
333
+ seen = Object.fromEntries(Object.entries(seen).filter(([key]) => key !== check.key));
334
+ else {
335
+ seen[check.key] = pair;
336
+ this.log.write("warning", `task: configured "${check.config}" "${check.configured}" ignored, ` +
337
+ `as store "${this.base}" uses "${check.effective}" for project "${this.prjId}" ` +
338
+ `(align the configuration via "ase config --scope project set ${check.config} ${check.effective}", ` +
339
+ `or switch the store via "ase task ${check.cmd} ${check.configured}"; reported once only)`);
340
+ }
341
+ }
342
+ if (!changed)
343
+ return;
344
+ try {
345
+ fs.mkdirSync(path.dirname(file), { recursive: true });
346
+ fs.writeFileSync(file, JSON.stringify(seen, null, 4) + "\n", "utf8");
347
+ }
348
+ catch {
349
+ /* best-effort only (the warning then repeats) */
350
+ }
351
+ }
352
+ async close() {
353
+ await this.dispatcher?.close();
354
+ }
355
+ async list(fields) {
356
+ const url = this.tasks + (fields !== undefined ? `?fields=${fields}` : "");
357
+ return (await this.request("GET", url) ?? this.unregistered()).tasks;
358
+ }
359
+ async load(id) {
360
+ return this.task(await this.request("GET", `${this.tasks}/${encodeURIComponent(id)}`));
361
+ }
362
+ async save(id, plan, tag, create = false) {
363
+ const result = await this.request("PUT", `${this.tasks}/${encodeURIComponent(id)}`, plan, {
364
+ ...(tag !== undefined ? { "If-Match": `"${tag}"` } : {}),
365
+ ...(create ? { "If-None-Match": "*" } : {})
366
+ });
367
+ if (result === null)
368
+ this.unregistered();
369
+ return result.tag;
370
+ }
371
+ /* allocate the next free task id on the server, falling back to determining
372
+ it locally (without any reservation) on a server predating the allocation */
373
+ async newId(title, proposal, taken) {
374
+ const result = await this.request("POST", `/projects/${this.prjId}/newid`, { title, proposal, taken });
375
+ if (result !== null)
376
+ return result;
377
+ const project = await this.request("GET", `/projects/${this.prjId}`) ?? this.unregistered();
378
+ const spec = project.idscheme ?? this.idscheme;
379
+ const ids = [...(await this.list()).map((item) => item.id), ...taken];
380
+ try {
381
+ return { scheme: spec, id: TaskFormat.nextTaskId(TaskFormat.parseIdScheme(spec), ids, title, proposal, project.seqmark ?? 0) };
382
+ }
383
+ catch (err) {
384
+ throw new Core.Problem(422, err instanceof Error ? err.message : String(err));
385
+ }
386
+ }
387
+ async patch(id, change) {
388
+ return this.task(await this.request("PATCH", `${this.tasks}/${encodeURIComponent(id)}`, change));
389
+ }
390
+ async delete(id) {
391
+ return await this.task(await this.request("DELETE", `${this.tasks}/${encodeURIComponent(id)}`)) !== null;
392
+ }
393
+ async purge(age) {
394
+ const result = await this.request("DELETE", `${this.tasks}?age=${encodeURIComponent(age)}`);
395
+ return (result ?? this.unregistered()).purged;
396
+ }
397
+ async attach(id, attachment, append = false) {
398
+ return await this.task(await this.request("POST", `${this.tasks}/${encodeURIComponent(id)}/attachment${append ? "?append=true" : ""}`, attachment)) !== null;
399
+ }
400
+ async content(id, index) {
401
+ const r = await ofetch.raw(`${this.base}${this.tasks}/${encodeURIComponent(id)}/attachment/${index}/content`, {
402
+ headers: { Authorization: `Bearer ${this.token}` },
403
+ responseType: "arrayBuffer",
404
+ dispatcher: this.dispatcher,
405
+ signal: AbortSignal.timeout(10000),
406
+ ignoreResponseError: true
407
+ }).catch((err) => this.unreachable(err));
408
+ if (r.status === 404)
409
+ return this.task(null);
410
+ if (r.status < 200 || r.status >= 300)
411
+ throw new Core.Problem(r.status, `store "${this.base}": HTTP ${r.status}`);
412
+ return { type: r.headers.get("content-type") ?? "application/octet-stream", content: Buffer.from(r._data ?? new ArrayBuffer(0)) };
413
+ }
414
+ /* subscribe to the change events of the project via the WebSocket event
415
+ endpoint, reconnecting after a connection loss, a failed or timed out
416
+ handshake, or a connection without any traffic despite keepalive pings
417
+ (e.g. a half-open one after a network partition), with a change notification
418
+ on reconnect (as events may have been missed, incl. lifecycle model changes),
419
+ reporting each change of the connection state (initially disconnected);
420
+ returns a function to unsubscribe */
421
+ subscribe(onChange, onState) {
422
+ const url = `${this.base.replace(/^http/, "ws")}/projects/${this.prjId}/events`;
423
+ let ws = null;
424
+ let timer = null;
425
+ let stopped = false;
426
+ let failed = false;
427
+ const connect = () => {
428
+ const sock = new WebSocket(url, {
429
+ headers: { Authorization: `Bearer ${this.token}` },
430
+ rejectUnauthorized: !this.insecure,
431
+ handshakeTimeout: HANDSHAKE_MS
432
+ });
433
+ ws = sock;
434
+ /* keep the connection alive: record any traffic (the frames, and the pings
435
+ of the task store and the pongs of our own pings), and terminate the
436
+ connection if there was none since the previous (then unanswered) ping */
437
+ let traffic = true;
438
+ let watchdog = null;
439
+ const alive = () => { traffic = true; };
440
+ sock.on("ping", alive);
441
+ sock.on("pong", alive);
442
+ sock.on("open", () => {
443
+ watchdog = setInterval(() => {
444
+ if (!traffic) {
445
+ this.log.write("debug", `task: store "${this.base}": events: connection unresponsive -- reconnecting`);
446
+ sock.terminate();
447
+ return;
448
+ }
449
+ traffic = false;
450
+ sock.ping();
451
+ }, KEEPALIVE_MS);
452
+ if (failed) {
453
+ RemoteTaskStoreClient.registered.delete(this.key);
454
+ onChange();
455
+ }
456
+ failed = false;
457
+ onState?.(true);
458
+ });
459
+ sock.on("message", (data) => {
460
+ traffic = true;
461
+ /* drop the cached registration on a lifecycle model or task id scheme change */
462
+ try {
463
+ const frame = JSON.parse(String(data));
464
+ if (frame.lifecycle !== undefined || frame.idscheme !== undefined)
465
+ RemoteTaskStoreClient.registered.delete(this.key);
466
+ }
467
+ catch {
468
+ /* ignore malformed frames (the change is notified anyway) */
469
+ }
470
+ onChange();
471
+ });
472
+ sock.on("error", (err) => {
473
+ this.log.write("debug", `task: store "${this.base}": events: ${err.message}`);
474
+ });
475
+ sock.on("close", () => {
476
+ if (watchdog !== null)
477
+ clearInterval(watchdog);
478
+ ws = null;
479
+ failed = true;
480
+ if (!stopped) {
481
+ onState?.(false);
482
+ timer = setTimeout(connect, 2000);
483
+ }
484
+ });
485
+ };
486
+ connect();
487
+ return () => {
488
+ stopped = true;
489
+ if (timer !== null)
490
+ clearTimeout(timer);
491
+ ws?.close();
492
+ this.close().catch(() => { });
493
+ };
494
+ }
495
+ }