@rse/ase 1.0.6 → 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 +230 -116
  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,581 @@
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 { Task } from "./ase-task.js";
7
+ import * as TaskFormat from "./ase-task-format.js";
8
+ import { Agent, tagsOf, taggedWith, flatKeys, setPlanTag, updateTask } from "./ase-agent-runtime.js";
9
+ /* the pseudo state of an outcome restoring the status of a task before its claim
10
+ (i.e., the incoming state it was claimed from) */
11
+ export const statePrevious = "PREVIOUS";
12
+ /* restore the previous assignee of a task plan (or clear it with an empty <assignee>),
13
+ but only if it is still assigned to the agent <name> (and not meanwhile to somebody
14
+ else, e.g. a human or the editor agent) */
15
+ const reassign = (plan, name, assignee) => {
16
+ if (String(plan.header.Assignee ?? "").trim() !== name)
17
+ return;
18
+ if (assignee !== "")
19
+ plan.header.Assignee = assignee;
20
+ else
21
+ delete plan.header.Assignee;
22
+ };
23
+ /* the agent driving one phase of the "enterprise" task lifecycle model:
24
+ it claims every task in one of the "incoming" states, which does not carry the tag
25
+ "ase-agent-<kind>" yet, whose "After:" predecessors all passed the phase, and
26
+ which is either still unassigned (unless the agent requires the assignment) or
27
+ already assigned to the agent, into the "activity" state, processes it via an ASE skill of the headless agent
28
+ tool, tags it, and moves it into the state decided by the concrete agent, or,
29
+ processing the tasks unclaimed (see "unclaimed"), it processes every task in its
30
+ states, which does not carry the tag yet, without claiming it, and just tags it */
31
+ export class PhaseAgent extends Agent {
32
+ /* the states of the tasks processed *unclaimed* (each adapted onto the task lifecycle
33
+ model of the project), or none to claim the tasks of the "incoming" state (by default):
34
+ an unclaimed task is selected independent of its assignee and its predecessors (see
35
+ "observe"), picked without any change of it (see "pick"), and settled by its tag only
36
+ (see "annotate"), so its status and its assignee are never changed and no other agent
37
+ is kept off it, where hence the skill itself has to save its result conditionally, as
38
+ another agent can claim and change the task meanwhile */
39
+ unclaimed = [];
40
+ /* whether only tasks already assigned to the agent (with its name as their
41
+ "Assignee:", e.g. explicitly by a human) are claimed, instead of also
42
+ the still unassigned tasks */
43
+ assigned = false;
44
+ /* whether an outcome of the agent can restore the status of a task before its claim
45
+ (see "statePrevious"), so a task which the skill itself moved back into one of the
46
+ "incoming" states (e.g. "ase-task-reboot", which resets it into the default state)
47
+ is still held by the agent */
48
+ restoring = false;
49
+ /* the status of each claimed task before its claim (one of the "incoming" states) */
50
+ origins = new Map();
51
+ /* whether the job of the agent finishes the result of the skill afterwards (e.g. by
52
+ committing and publishing it), so a task already in the "success" state does not
53
+ prove the finishing (see "recover"), where a concrete agent can decide on it
54
+ on its initialization (e.g. by its mode) */
55
+ finishing = false;
56
+ /* the assignee under which the agent claims, holds, recognizes, and releases
57
+ the tasks (default: its name) */
58
+ get assignee() {
59
+ return this.name;
60
+ }
61
+ /* whether a task is dispatchable to the agent by its assignee: a task
62
+ pre-assigned to the agent always is, an unassigned task only if the
63
+ agent does not require the assignment, and a task assigned to anybody
64
+ else (a human or another agent) never is */
65
+ assignedTo(keys) {
66
+ const assignee = (keys.get("Assignee") ?? "").trim();
67
+ return assignee === this.assignee || (assignee === "" && !this.assigned);
68
+ }
69
+ /* decide whether a processed task, moved into the state <to> and tagged with the value
70
+ <value> of its label, was processed successfully (for the level and the wording of
71
+ the result log line): by default, if moved into the "success" state, labeled, or untagged */
72
+ succeeded(to, value, _task) {
73
+ return to === this.states.success || value !== undefined;
74
+ }
75
+ /* decide on the assignee to hand a processed task <task> over to, by its success
76
+ and its previous assignee <previous> before the claim (or empty to unassign it):
77
+ by default, a processed task is unassigned */
78
+ handover(_success, _previous, _task) {
79
+ return "";
80
+ }
81
+ /* decide on the value of the tag of a processed task (e.g. a verdict of the
82
+ skill) by its success and its output lines, or none (undefined) for a plain
83
+ tag, or no tag at all (null), if the result of the processing itself (e.g. an
84
+ attachment) keeps the agent from claiming the task again (see "ready") */
85
+ label(_ok, _output, _task) {
86
+ return undefined;
87
+ }
88
+ /* the tag marking the tasks picked up by any agent of its kind: with the
89
+ value "active" ("ase-agent-<kind>:active") from the claim on, and plain
90
+ ("ase-agent-<kind>") or with the value of its label (e.g. a verdict, like
91
+ "ase-agent-qualifier:approve") once processed, so a task which the skill itself
92
+ puts back into the "incoming" state during its processing (e.g.
93
+ "ase-task-reboot", which takes over the tags) is never picked up again
94
+ by a peer agent and does not count as passed for its successors yet,
95
+ where an agent can use a tag of its own per mode (e.g. "ase-agent-implementer-draft") */
96
+ get tag() {
97
+ return `ase-agent-${this.kind}`;
98
+ }
99
+ /* the description of the tag value <value> of a processed task (see "label") */
100
+ tagged(value) {
101
+ return value === null ? "untagged" : `tag "${value !== undefined ? `${this.tag}:${value}` : this.tag}"`;
102
+ }
103
+ /* the states of the tasks processed unclaimed, adapted onto the task lifecycle model */
104
+ observed() {
105
+ const lifecycle = this.lifecycle;
106
+ return new Set(this.unclaimed.map((state) => TaskFormat.adaptStatus(lifecycle, state)));
107
+ }
108
+ /* whether a predecessor <pred> (known and not cancelled) with its <status> (adapted
109
+ to the lifecycle model) passed the phase for its successor <item>: by default once
110
+ in or beyond the "passed" state, or in the "success" state and tagged as processed,
111
+ where a predecessor still held by an agent of its kind (tagged as "active") has not
112
+ passed the phase yet while in the "success" state or not finished, even if the skill
113
+ already moved it into or beyond the "passed" state, as the agent still finishes it
114
+ (e.g. commits and publishes its change set) and might still fail it, and where a
115
+ predecessor whose finishing failed (tagged as "failed") has not passed the phase
116
+ yet while still in the "success" state, but has once moved on beyond it (e.g. after
117
+ a manual intervention) */
118
+ passes(pred, status, _item) {
119
+ const lifecycle = this.lifecycle;
120
+ const { success, passed } = this.states;
121
+ const index = (status) => lifecycle.states.indexOf(status);
122
+ const tags = tagsOf(pred.keys);
123
+ if (status === success)
124
+ return !tags.includes(`${this.tag}:active`) && !tags.includes(`${this.tag}:failed`)
125
+ && (index(status) >= index(passed) || taggedWith(pred.keys, this.tag));
126
+ else
127
+ return index(status) >= index(passed)
128
+ && (!tags.includes(`${this.tag}:active`) || lifecycle.finished.includes(status));
129
+ }
130
+ /* whether a task has predecessors: those of its "After:" key, or, as an
131
+ explicit epic (a task being its own group), the members of its group */
132
+ dependent(task) {
133
+ return (task.keys.get("After") ?? "").trim() !== "" || (task.keys.get("Group") ?? "").trim() === task.id;
134
+ }
135
+ /* whether a task in the status <status> is still held in the phase by the agent: in
136
+ the "activity" state, or, for a restoring agent, back in one of the "incoming" states */
137
+ held(status) {
138
+ return status === this.states.activity || (this.restoring && this.states.incoming.includes(status));
139
+ }
140
+ /* select the dispatchable tasks among all listed tasks <items> (except the
141
+ <busy> ones already processed in a worker slot), the tasks waiting (with the
142
+ reason), and the tasks with a notice: by default every task in one of the "incoming"
143
+ states, which is not tagged yet, is dispatchable to the agent by its assignee,
144
+ and whose predecessors all passed the phase (see "passes") or no longer exist,
145
+ else waiting, where the predecessors of a task are those of its "After:" key and,
146
+ for an explicit epic (a task being its own group), implicitly all other tasks of
147
+ its group, where a predecessor with a status foreign to the lifecycle model is
148
+ judged by the status it maps onto, where a cancelled predecessor of the "After:"
149
+ key never passes (as the successor might depend on its never delivered result,
150
+ so the successor waits until a human resolves the dependency), while a cancelled
151
+ member of an epic counts as dropped from the epic (and hence as passed), and
152
+ where a task with unknown or cancelled predecessors, or waiting in a dependency
153
+ cycle (both of which never resolve by themselves), is noticed, while the tasks
154
+ processed unclaimed are selected otherwise (see "observe") */
155
+ select(items, busy) {
156
+ if (this.unclaimed.length > 0)
157
+ return this.observe(items, busy);
158
+ const lifecycle = this.lifecycle;
159
+ const { incoming } = this.states;
160
+ const known = new Map(items.map((item) => [item.id, item]));
161
+ const waiting = new Map();
162
+ const notices = new Map();
163
+ const tasks = [];
164
+ /* determine the explicit predecessors of all tasks, and the
165
+ implicit ones of the explicit epics (their group members) */
166
+ const members = new Map();
167
+ for (const item of items) {
168
+ const group = (item.keys.get("Group") ?? "").trim();
169
+ if (group !== "" && group !== item.id)
170
+ members.set(group, [...(members.get(group) ?? []), item.id]);
171
+ }
172
+ const explicit = new Map(items.map((item) => [item.id, [...new Set((item.keys.get("After") ?? "").split(",")
173
+ .map((after) => after.trim())
174
+ .filter((after) => after !== ""))]]));
175
+ const implicit = new Map(items.map((item) => [item.id, (item.keys.get("Group") ?? "").trim() === item.id ?
176
+ (members.get(item.id) ?? []).filter((member) => !explicit.get(item.id).includes(member)) : []]));
177
+ const preds = (id) => [...(explicit.get(id) ?? []), ...(implicit.get(id) ?? [])];
178
+ /* determine whether a (known) predecessor passed the phase for its successor */
179
+ const cancelled = (id) => TaskFormat.adaptStatus(lifecycle, known.get(id).status) === "CANCELLED";
180
+ const done = (id, succ) => {
181
+ const pred = known.get(id);
182
+ const status = TaskFormat.adaptStatus(lifecycle, pred.status);
183
+ if (status === "CANCELLED")
184
+ return !explicit.get(succ).includes(id);
185
+ return this.passes(pred, status, known.get(succ));
186
+ };
187
+ /* determine the dependency cycle through a task along the predecessors
188
+ not passed yet (as a path from the task back to itself, else empty) */
189
+ const cycle = (id) => {
190
+ const seen = new Set();
191
+ const walk = (path) => {
192
+ const last = path[path.length - 1];
193
+ for (const pred of preds(last)) {
194
+ if (pred === id)
195
+ return [...path, id];
196
+ if (!known.has(pred) || seen.has(pred) || done(pred, last))
197
+ continue;
198
+ seen.add(pred);
199
+ const found = walk([...path, pred]);
200
+ if (found.length > 0)
201
+ return found;
202
+ }
203
+ return [];
204
+ };
205
+ return walk([id]);
206
+ };
207
+ /* select the tasks */
208
+ const quoted = (ids) => ids.map((id) => `"${id}"`);
209
+ for (const item of items.filter((item) => incoming.includes(item.status)
210
+ && !taggedWith(item.keys, this.tag) && this.assignedTo(item.keys) && !busy.has(item.id))) {
211
+ const after = preds(item.id);
212
+ const unknown = after.filter((pred) => !known.has(pred));
213
+ const pending = after.filter((pred) => known.has(pred) && !done(pred, item.id));
214
+ const dropped = pending.filter((pred) => cancelled(pred));
215
+ const cyclic = pending.length > 0 ? cycle(item.id) : [];
216
+ const notes = [];
217
+ if (unknown.length > 0)
218
+ notes.push(`references the unknown predecessors ${quoted(unknown).join(", ")} (treated as passed)`);
219
+ if (dropped.length > 0)
220
+ notes.push(`waits for the cancelled predecessors ${quoted(dropped).join(", ")}, ` +
221
+ "which never pass (remove them from its \"After:\" key, or cancel the task, too)");
222
+ if (cyclic.length > 0)
223
+ notes.push(`waits in the dependency cycle ${quoted(cyclic).join(" → ")} (each after the next), ` +
224
+ "which never resolves by itself");
225
+ if (notes.length > 0)
226
+ notices.set(item.id, notes.join(", and "));
227
+ if (pending.length === 0)
228
+ tasks.push({ id: item.id, title: item.title, keys: item.keys });
229
+ else
230
+ waiting.set(item.id, "its predecessors " +
231
+ pending.map((pred) => `"${pred}" (${known.get(pred).status})`).join(", "));
232
+ }
233
+ return { tasks, waiting, notices };
234
+ }
235
+ /* select the tasks processed unclaimed: every task in one of the unclaimed states (by
236
+ its status adapted onto the lifecycle model), which is not tagged yet, independent of
237
+ its assignee and its predecessors (as its processing never progresses its lifecycle),
238
+ except one whose finalization is still retried (as it is not tagged yet) */
239
+ observe(items, busy) {
240
+ const lifecycle = this.lifecycle;
241
+ const states = this.observed();
242
+ const tasks = items
243
+ .filter((item) => states.has(TaskFormat.adaptStatus(lifecycle, item.status))
244
+ && !taggedWith(item.keys, this.tag) && !busy.has(item.id) && !this.unsettled.has(item.id))
245
+ .map((item) => ({ id: item.id, title: item.title, keys: item.keys }));
246
+ return { tasks, waiting: new Map() };
247
+ }
248
+ /* claim a selected task, returning the description of the claim (for the processing
249
+ log), or null if the task was skipped (throws if the claim failed):
250
+ by default the claim is won only if the task is still in one of the "incoming" states,
251
+ and the task is then moved into the "activity" state, tagged as "active", and assigned
252
+ to the agent, all in a single conditional update of the task (retried on a concurrent
253
+ change of it), so a failed claim never leaves the task half-claimed, and the claim
254
+ is recorded for the run of the agent, so it is recovered after a termination,
255
+ while a task processed unclaimed is just picked (see "pick") */
256
+ async claim(task) {
257
+ if (this.unclaimed.length > 0)
258
+ return this.pick(task);
259
+ const { incoming, activity } = this.states;
260
+ /* a task with predecessors is re-checked against a fresh listing right before
261
+ its claim, as its predecessors might have changed since the listing of the
262
+ dispatching (e.g. while the task was prepared, see "ready"), so a task no
263
+ longer dispatchable is silently skipped (and reported as waiting by the next
264
+ dispatching, triggered by the change of its predecessors) */
265
+ if (this.dependent(task)) {
266
+ const { items } = await Task.listHeaders(this.log);
267
+ if (!this.select(items, new Set()).tasks.some((selected) => selected.id === task.id))
268
+ return null;
269
+ }
270
+ /* a task whose status changed meanwhile is skipped, while a task tagged
271
+ meanwhile was already picked up by an agent of this kind (e.g. a peer agent,
272
+ after the listing) and a task (re)assigned meanwhile is no longer for the
273
+ agent, so both are silently skipped, and any other task is claimed (with its
274
+ current header keys, including the previous assignee to restore on interrupt) */
275
+ let from = "";
276
+ let origin = "";
277
+ let skipped = "";
278
+ let keys = task.keys;
279
+ /* the header keys of the task of the last attempted claim which might have been saved,
280
+ as a retried update after a claim saved, but whose response was lost, finds the
281
+ task already claimed by itself (and not by another agent of the same name, as
282
+ the attempt is reset whenever its save was surely rejected) */
283
+ let attempted = null;
284
+ try {
285
+ const exists = await updateTask(this.log, task.id, (plan, lifecycle) => {
286
+ from = TaskFormat.taskStatus(plan.header, lifecycle);
287
+ const current = flatKeys(plan.header);
288
+ if (attempted !== null && from === activity && tagsOf(current).includes(`${this.tag}:active`)
289
+ && (current.get("Assignee") ?? "").trim() === this.assignee) {
290
+ keys = attempted;
291
+ skipped = "";
292
+ return false;
293
+ }
294
+ keys = current;
295
+ skipped = !incoming.includes(from) ? "status" :
296
+ taggedWith(keys, this.tag) || !this.assignedTo(keys) ? "silently" : "";
297
+ if (skipped !== "")
298
+ return false;
299
+ origin = from;
300
+ this.runLock.claim(task.id, {
301
+ name: this.assignee, tag: this.tag, assignee: (keys.get("Assignee") ?? "").trim(), status: origin
302
+ });
303
+ plan.header.Status = activity;
304
+ setPlanTag(plan, this.tag, "active");
305
+ plan.header.Assignee = this.assignee;
306
+ attempted = keys;
307
+ return true;
308
+ }, () => {
309
+ attempted = null;
310
+ });
311
+ if (!exists)
312
+ skipped = "silently";
313
+ }
314
+ catch (err) {
315
+ /* the claim is still recorded if it might have been saved (as its
316
+ response was lost), so the task is recovered after the run */
317
+ if (attempted === null)
318
+ this.runLock.unclaim(task.id);
319
+ throw err;
320
+ }
321
+ if (skipped !== "") {
322
+ this.runLock.unclaim(task.id);
323
+ if (skipped === "status")
324
+ await this.write("warning", this.name, `task "${task.id}" skipped, as its status changed meanwhile to "${from}"`);
325
+ return null;
326
+ }
327
+ task.keys = keys;
328
+ this.origins.set(task.id, origin);
329
+ return `claimed task (status "${origin}" → "${activity}")`;
330
+ }
331
+ /* pick a selected task for its unclaimed processing, returning the description of the
332
+ pick (for the processing log), or null if the task was skipped: the task is re-checked
333
+ against its current plan (as it might have changed since the listing of the
334
+ dispatching, e.g. by its claim through another agent), but never changed, and the pick
335
+ is only recorded for the run of the agent, so no peer agent of its kind on this host
336
+ picks it up meanwhile (while peer agents on other hosts are not coordinated, as the
337
+ conditional save of the result by the skill keeps their processings apart) */
338
+ async pick(task) {
339
+ const current = await Task.inspect(this.log, task.id, (plan, lifecycle) => ({
340
+ status: TaskFormat.adaptStatus(lifecycle, TaskFormat.taskStatus(plan.header, lifecycle)),
341
+ keys: flatKeys(plan.header)
342
+ }));
343
+ if (current === null || taggedWith(current.keys, this.tag))
344
+ return null;
345
+ else if (!this.observed().has(current.status)) {
346
+ await this.write("info", this.name, `task "${task.id}" skipped, as its status changed meanwhile to "${current.status}"`);
347
+ return null;
348
+ }
349
+ else if (await this.runLock.claimed(task.id))
350
+ return null;
351
+ this.runLock.claim(task.id, { name: this.assignee, tag: this.tag, assignee: (current.keys.get("Assignee") ?? "").trim() });
352
+ task.keys = current.keys;
353
+ return `picked task unclaimed (status "${current.status}")`;
354
+ }
355
+ /* settle a processed task after the skill invocation (successful by <ok>,
356
+ with the job finished by <finished>, or interrupted by <aborted>), by
357
+ default deciding on its status: a task already in the "success" state is
358
+ tagged plain or labeled, unassigned, and kept, a task still held in the phase (see "held")
359
+ returns untagged (and with its previous assignee) into its "incoming" state of the claim
360
+ on interrupt and is otherwise tagged plain or labeled, unassigned, and moved into the
361
+ state of the outcome (where "statePrevious" restores its "incoming" state of the claim), where a task already in the "success" state, but whose
362
+ finishing failed (e.g. one already "INTEGRATED" resp. "IMPLEMENTED" by the
363
+ skill, but whose publishing of the merge resp. of the changeset branch was
364
+ rejected), and a task whose outcome is not reachable from its state, is
365
+ tagged as "failed", unassigned, and left in its state for a manual intervention
366
+ (instead of being parked, e.g. "STALLED", which is reachable from "IMPLEMENTED"
367
+ via "QUALIFYING" and "PLANNED" and would hide the failure), while a task processed
368
+ unclaimed is just tagged (see "annotate") */
369
+ async settle(task, ok, finished, aborted, output, log) {
370
+ if (this.unclaimed.length > 0)
371
+ return this.annotate(task, ok, aborted, output, log);
372
+ const { incoming, activity, success } = this.states;
373
+ /* the tag is made plain or labeled (and the assignee cleared, or handed over) together
374
+ with the status change, in a single conditional update of the task (retried on a
375
+ concurrent change of it), so no agent of its kind claims a processed task again, and a
376
+ processing with a label (e.g. a verdict) or without a tag (as its result itself
377
+ guards it) succeeded, even if not moved into the "success" state, where the
378
+ assignee is only cleared (resp. restored, or handed over) if the task is still
379
+ assigned to the agent (and not meanwhile, e.g., to the editor agent), and where a
380
+ task already in the "success" state counts as processed by the skill only if the
381
+ "success" state differs from the "activity" state (else it is decided by the outcome) */
382
+ const value = aborted ? undefined : this.label(ok, output, task);
383
+ const assignee = (task.keys.get("Assignee") ?? "").trim();
384
+ const origin = this.origins.get(task.id) ?? incoming[0];
385
+ let level = "info";
386
+ let result = "";
387
+ const finish = () => updateTask(this.log, task.id, (plan, lifecycle) => {
388
+ const status = TaskFormat.taskStatus(plan.header, lifecycle);
389
+ const held = this.held(status);
390
+ if (held && aborted) {
391
+ setPlanTag(plan, this.tag, null);
392
+ reassign(plan, this.assignee, assignee);
393
+ plan.header.Status = origin;
394
+ level = "warning";
395
+ result = `task released (status "${status}" → "${origin}", untagged)`;
396
+ }
397
+ else if (status === success && status !== activity && finished && !held) {
398
+ const good = this.succeeded(success, value, task);
399
+ setPlanTag(plan, this.tag, value);
400
+ reassign(plan, this.assignee, this.handover(good, assignee, task));
401
+ level = good ? "info" : "warning";
402
+ result = `task processed ${good ? "successfully" : "unsuccessfully"} (status "${success}", ${this.tagged(value)})`;
403
+ }
404
+ else if (held || status === success) {
405
+ const outcome = this.outcome(ok, output, task);
406
+ const to = outcome === statePrevious ? origin : outcome;
407
+ if (status === success && !held) {
408
+ /* the skill succeeded, but the finishing failed (as "finished" is
409
+ false here), so the result of the skill (e.g. a merge or a commit
410
+ not published) must not be hidden by parking the task */
411
+ setPlanTag(plan, this.tag, "failed");
412
+ reassign(plan, this.assignee, "");
413
+ level = "error";
414
+ result = `task processed, but its finishing failed -- left in "${status}" ` +
415
+ `for a manual intervention (${this.tagged("failed")})`;
416
+ }
417
+ else if (TaskFormat.checkStatus(lifecycle, status, to) !== "") {
418
+ setPlanTag(plan, this.tag, "failed");
419
+ reassign(plan, this.assignee, "");
420
+ level = "error";
421
+ result = `task processed, but its status "${status}" cannot change into "${to}" ` +
422
+ `-- left in "${status}" for a manual intervention (${this.tagged("failed")})`;
423
+ }
424
+ else {
425
+ const good = this.succeeded(to, value, task);
426
+ setPlanTag(plan, this.tag, value);
427
+ reassign(plan, this.assignee, this.handover(good, assignee, task));
428
+ plan.header.Status = to;
429
+ level = good ? "info" : "warning";
430
+ result = `task processed ${good ? "successfully" : "unsuccessfully"} ` +
431
+ `(status "${status}" → "${to}", ${this.tagged(value)})`;
432
+ }
433
+ }
434
+ else {
435
+ /* tagged, too, as a skill can itself move the task back into the
436
+ "incoming" state (e.g. "ase-task-integrate" on nothing to integrate) */
437
+ setPlanTag(plan, this.tag, value);
438
+ reassign(plan, this.assignee, this.handover(this.succeeded(status, value, task), assignee, task));
439
+ level = "warning";
440
+ result = `task status changed meanwhile to "${status}" -- left untouched (${this.tagged(value)})`;
441
+ }
442
+ return true;
443
+ });
444
+ await this.finalize(task, finish, () => ({ level, result }), log);
445
+ }
446
+ /* settle a task processed unclaimed by its tag only (see "label"), in a single
447
+ conditional update of the task (retried on a concurrent change of it), which is
448
+ saved only if its tag changes, so neither its status nor its assignee is ever
449
+ changed (also if the skill itself moved it elsewhere meanwhile), where a task whose
450
+ processing was interrupted is left untouched */
451
+ async annotate(task, ok, aborted, output, log) {
452
+ if (aborted) {
453
+ this.runLock.unclaim(task.id);
454
+ await log("warning", "task released (processing interrupted, task left untouched)");
455
+ return;
456
+ }
457
+ const value = this.label(ok, output, task);
458
+ let level = "info";
459
+ let result = "";
460
+ const finish = () => updateTask(this.log, task.id, (plan, lifecycle) => {
461
+ const status = TaskFormat.taskStatus(plan.header, lifecycle);
462
+ const tags = () => tagsOf(flatKeys(plan.header)).join(",");
463
+ const before = tags();
464
+ const good = this.succeeded(status, value, task);
465
+ setPlanTag(plan, this.tag, value);
466
+ level = good ? "info" : "warning";
467
+ result = `task processed ${good ? "successfully" : "unsuccessfully"} ` +
468
+ `(status "${status}" kept, ${this.tagged(value)})`;
469
+ return tags() !== before;
470
+ });
471
+ await this.finalize(task, finish, () => ({ level, result }), log);
472
+ }
473
+ /* finalize a processed task by the settling update <finish> (resolving to whether the
474
+ task still exists) and log its result of <report>, where a finalization failing even
475
+ after its retries is retried in the background (see "unsettle"), while the claim stays
476
+ recorded for the run, so the task never stays in the "activity" state (and is
477
+ recovered after a termination meanwhile) */
478
+ async finalize(task, finish, report, log) {
479
+ let exists;
480
+ try {
481
+ exists = await finish();
482
+ }
483
+ catch (err) {
484
+ this.unsettle(task.id, async () => {
485
+ const exists = await finish();
486
+ const { level, result } = report();
487
+ this.runLock.unclaim(task.id);
488
+ this.origins.delete(task.id);
489
+ await this.write(exists ? level : "warning", `${this.name}: ${task.id}`, exists ? `${result} (finalization retried)` : "task vanished -- finalization dropped", undefined, task.id);
490
+ return true;
491
+ });
492
+ await log("error", "finalizing the task failed (retried later): " +
493
+ `${err instanceof Error ? err.message : String(err)}`);
494
+ return;
495
+ }
496
+ this.runLock.unclaim(task.id);
497
+ this.origins.delete(task.id);
498
+ const { level, result } = report();
499
+ if (exists)
500
+ await log(level, result);
501
+ else
502
+ await log("warning", "task vanished meanwhile");
503
+ }
504
+ /* recover the tasks left behind by terminated runs of agents of its kind (e.g. killed
505
+ ones), as recorded for their runs, where each terminated run is seized exclusively
506
+ (so concurrently started agents never recover the same run), and a task is recovered
507
+ only if still owned by the terminated agent, i.e., still tagged as "active" and assigned
508
+ to it, and not claimed meanwhile by a live agent of this host (e.g. after its release
509
+ by an earlier recovery, or after its settling right before the termination): such a
510
+ task is released (like on an interrupt) if still held in the phase (see "held"), tagged as
511
+ "failed" (like on a failed finishing, see "settle") if in the "success" state of an
512
+ agent whose job finishes the result of the skill, as the finishing (e.g. the publishing
513
+ of the change set) might never have run, and else tagged plain (as processed), where
514
+ each recovered claim is removed from the record of the run, and the record of a run
515
+ is removed once all its tasks are recovered */
516
+ async recover() {
517
+ const { incoming, success } = this.states;
518
+ for (const { run } of await this.runLock.dead()) {
519
+ const claims = await this.runLock.seize(run);
520
+ if (claims === null)
521
+ continue;
522
+ let recovered = true;
523
+ for (const [id, claim] of Object.entries(claims)) {
524
+ let result = "";
525
+ try {
526
+ if (await this.runLock.claimed(id)) {
527
+ this.runLock.forget(run, id);
528
+ continue;
529
+ }
530
+ await updateTask(this.log, id, (plan, lifecycle) => {
531
+ const status = TaskFormat.taskStatus(plan.header, lifecycle);
532
+ result = "";
533
+ if (!tagsOf(flatKeys(plan.header)).includes(`${claim.tag}:active`)
534
+ || String(plan.header.Assignee ?? "").trim() !== claim.name)
535
+ return false;
536
+ else if (this.held(status)) {
537
+ const origin = claim.status !== undefined && incoming.includes(claim.status) ? claim.status : incoming[0];
538
+ setPlanTag(plan, claim.tag, null);
539
+ reassign(plan, claim.name, claim.assignee);
540
+ plan.header.Status = origin;
541
+ result = `released (status "${status}" → "${origin}", untagged)`;
542
+ }
543
+ else if (status === success && this.finishing) {
544
+ setPlanTag(plan, claim.tag, "failed");
545
+ reassign(plan, claim.name, "");
546
+ result = `processed, but its finishing might not have run -- left in "${status}" ` +
547
+ `for a manual intervention (tag "${claim.tag}:failed")`;
548
+ }
549
+ else {
550
+ setPlanTag(plan, claim.tag);
551
+ reassign(plan, claim.name, "");
552
+ result = `marked as processed (status "${status}", tag "${claim.tag}")`;
553
+ }
554
+ return true;
555
+ });
556
+ }
557
+ catch (err) {
558
+ recovered = false;
559
+ await this.write("warning", this.name, `task "${id}" of terminated ${this.role} "${claim.name}" ` +
560
+ `not recovered: ${err instanceof Error ? err.message : String(err)}`);
561
+ continue;
562
+ }
563
+ this.runLock.forget(run, id);
564
+ if (result !== "")
565
+ await this.write("warning", this.name, `task "${id}" of terminated ${this.role} "${claim.name}" ${result}`);
566
+ }
567
+ if (recovered)
568
+ await this.runLock.purge(run);
569
+ else
570
+ await this.runLock.free(run);
571
+ }
572
+ }
573
+ /* the description of the observed tasks (for the start log line) */
574
+ observing() {
575
+ if (this.unclaimed.length > 0)
576
+ return `${[...this.observed()].map((state) => `"${state}"`).join(", ")} tasks without tag "${this.tag}" ` +
577
+ "(unclaimed, independent of their assignee and predecessors)";
578
+ return `${this.states.incoming.map((state) => `"${state}"`).join(", ")} tasks without tag "${this.tag}" ` +
579
+ (this.assigned ? `and assigned to "${this.assignee}"` : `and unassigned or assigned to "${this.assignee}"`);
580
+ }
581
+ }