@rse/ase 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +357 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +8 -1
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +269 -81
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +231 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +81 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /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
|
+
}
|