@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,127 @@
|
|
|
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 { effortSizes } from "./ase-task-format.js";
|
|
7
|
+
/* shortest keyword still eligible for fuzzy matching: below it the
|
|
8
|
+
edit distance would equate it with almost any short word */
|
|
9
|
+
const fuzzyMin = 4;
|
|
10
|
+
/* the Levenshtein edit distance of two words */
|
|
11
|
+
const levenshtein = (a, b) => {
|
|
12
|
+
let prev = [];
|
|
13
|
+
let curr = [];
|
|
14
|
+
for (let j = 0; j <= b.length; j++)
|
|
15
|
+
prev[j] = j;
|
|
16
|
+
for (let i = 1; i <= a.length; i++) {
|
|
17
|
+
curr[0] = i;
|
|
18
|
+
for (let j = 1; j <= b.length; j++)
|
|
19
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
20
|
+
const swap = prev;
|
|
21
|
+
prev = curr;
|
|
22
|
+
curr = swap;
|
|
23
|
+
}
|
|
24
|
+
return prev[b.length];
|
|
25
|
+
};
|
|
26
|
+
/* the Sorensen-Dice bigram similarity coefficient of two words */
|
|
27
|
+
const diceCoefficient = (a, b) => {
|
|
28
|
+
if (a === b)
|
|
29
|
+
return 1;
|
|
30
|
+
if (a.length < 2 || b.length < 2)
|
|
31
|
+
return 0;
|
|
32
|
+
const bigrams = new Map();
|
|
33
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
34
|
+
const bigram = a.substring(i, i + 2);
|
|
35
|
+
bigrams.set(bigram, (bigrams.get(bigram) ?? 0) + 1);
|
|
36
|
+
}
|
|
37
|
+
let hits = 0;
|
|
38
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
39
|
+
const bigram = b.substring(i, i + 2);
|
|
40
|
+
const count = bigrams.get(bigram) ?? 0;
|
|
41
|
+
if (count > 0) {
|
|
42
|
+
bigrams.set(bigram, count - 1);
|
|
43
|
+
hits++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return (2 * hits) / (a.length + b.length - 2);
|
|
47
|
+
};
|
|
48
|
+
/* parse a filter query into its comma-separated OR-combined parts,
|
|
49
|
+
each a list of whitespace-separated AND-combined keywords */
|
|
50
|
+
export const parseFilter = (query) => query.toLowerCase().split(",")
|
|
51
|
+
.map((part) => part.split(/\s+/).filter((word) => word !== ""))
|
|
52
|
+
.filter((part) => part.length > 0);
|
|
53
|
+
/* the effort size of a keyword "effort:<size>" (of a lower-cased query) of a known
|
|
54
|
+
size, else null, as any other keyword (also one of an unknown size) is a regular one */
|
|
55
|
+
const effortTerm = (word) => effortSizes.find((size) => word === `effort:${size.toLowerCase()}`) ?? null;
|
|
56
|
+
/* determine the ids of the cards matching a filter query (as SpecBook
|
|
57
|
+
does): each part matches in up to two passes, the keywords as literal
|
|
58
|
+
substrings of task id and title first and, only if this matches no card
|
|
59
|
+
at all, as fuzzy variants of their words, as the edit-distance neighborhood
|
|
60
|
+
of a keyword would otherwise dilute a correctly spelled query, where a
|
|
61
|
+
keyword "effort:<size>" always exactly matches the cards of the effort size */
|
|
62
|
+
export const matchCards = (cards, query) => {
|
|
63
|
+
const units = cards.map((card) => {
|
|
64
|
+
const text = `${card.id} ${card.title}`.toLowerCase();
|
|
65
|
+
return { id: card.id, text, effort: card.effort, words: new Set(text.split(/[^\p{L}\p{N}]+/u).filter((w) => w !== "")) };
|
|
66
|
+
});
|
|
67
|
+
const vocab = new Set(units.flatMap((unit) => [...unit.words]));
|
|
68
|
+
/* match a keyword literally (resp. a keyword "effort:<size>" exactly) */
|
|
69
|
+
const literal = (unit, word) => {
|
|
70
|
+
const size = effortTerm(word);
|
|
71
|
+
return size !== null ? unit.effort === size : unit.text.includes(word);
|
|
72
|
+
};
|
|
73
|
+
/* the fuzzy-equivalent vocabulary words of a keyword, cached per keyword */
|
|
74
|
+
const fuzzy = new Map();
|
|
75
|
+
const fuzzyWords = (word) => {
|
|
76
|
+
let result = fuzzy.get(word);
|
|
77
|
+
if (result === undefined) {
|
|
78
|
+
result = word.length < fuzzyMin ? [] : [...vocab].filter((given) => Math.abs(word.length - given.length) <= 1
|
|
79
|
+
&& (diceCoefficient(word, given) >= 0.50 || levenshtein(word, given) <= 2));
|
|
80
|
+
fuzzy.set(word, result);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
const kept = new Set();
|
|
85
|
+
for (const part of parseFilter(query)) {
|
|
86
|
+
let matched = units.filter((unit) => part.every((word) => literal(unit, word)));
|
|
87
|
+
if (matched.length === 0)
|
|
88
|
+
matched = units.filter((unit) => part.every((word) => literal(unit, word)
|
|
89
|
+
|| (effortTerm(word) === null && fuzzyWords(word).some((v) => unit.words.has(v)))));
|
|
90
|
+
matched.forEach((unit) => kept.add(unit.id));
|
|
91
|
+
}
|
|
92
|
+
return kept;
|
|
93
|
+
};
|
|
94
|
+
/* reduce a board onto the cards matching a filter query (an empty query
|
|
95
|
+
keeps the board as-is), where for the graph the direct predecessors and
|
|
96
|
+
successors of the matching cards are kept as context cards, too */
|
|
97
|
+
export const filterBoard = (board, query, graph = false) => {
|
|
98
|
+
if (parseFilter(query).length === 0)
|
|
99
|
+
return board;
|
|
100
|
+
const matched = matchCards([...board.cards.values()], query);
|
|
101
|
+
const context = new Set();
|
|
102
|
+
if (graph)
|
|
103
|
+
for (const id of matched)
|
|
104
|
+
for (const ref of [...(board.pred.get(id) ?? []), ...(board.succ.get(id) ?? [])])
|
|
105
|
+
if (!matched.has(ref))
|
|
106
|
+
context.add(ref);
|
|
107
|
+
const kept = (id) => matched.has(id) || context.has(id);
|
|
108
|
+
const edges = (map) => new Map([...map].filter(([id]) => kept(id)).map(([id, refs]) => [id, refs.filter(kept)]));
|
|
109
|
+
return {
|
|
110
|
+
...board,
|
|
111
|
+
cards: new Map([...board.cards].filter(([id]) => kept(id))),
|
|
112
|
+
lanes: new Map([...board.lanes].map(([status, cards]) => [status, cards.filter((c) => kept(c.id))])),
|
|
113
|
+
pred: edges(board.pred),
|
|
114
|
+
succ: edges(board.succ),
|
|
115
|
+
context
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/* reduce a board onto the cards having any predecessors or successors,
|
|
119
|
+
i.e., drop the standalone cards (for the graph only) */
|
|
120
|
+
export const dropStandalone = (board) => {
|
|
121
|
+
const kept = (id) => (board.pred.get(id) ?? []).length + (board.succ.get(id) ?? []).length > 0;
|
|
122
|
+
return {
|
|
123
|
+
...board,
|
|
124
|
+
cards: new Map([...board.cards].filter(([id]) => kept(id))),
|
|
125
|
+
lanes: new Map([...board.lanes].map(([status, cards]) => [status, cards.filter((c) => kept(c.id))]))
|
|
126
|
+
};
|
|
127
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
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 * as TaskFormat from "./ase-task-format.js";
|
|
7
|
+
import { units, portSpacing, layoutBoxes, textGrid, drawContainers, drawContainerLabels, drawEdges, drawBox, drawSVGFrame, escapeXML } from "./ase-task-board-graph.js";
|
|
8
|
+
/* the id prefixes of the state nodes and of the agent cards of the agent graph
|
|
9
|
+
(where the ids of the edges, separated by "->", never contain this separator) */
|
|
10
|
+
const STATE = "state:";
|
|
11
|
+
const CARD = "agent:";
|
|
12
|
+
/* the sizes of an agent card: in cell units (two border lines around the name line
|
|
13
|
+
and three field lines, like the agent cards of the agents view), and in pixel units
|
|
14
|
+
(the size of the agent cards of the agents view: their 260px column without its
|
|
15
|
+
paddings and borders, and their name line and three field lines with their paddings and borders) */
|
|
16
|
+
const CARD_W = 32;
|
|
17
|
+
const CARD_H = 6;
|
|
18
|
+
const CARD_PX_W = 238;
|
|
19
|
+
const CARD_PX_H = 84;
|
|
20
|
+
/* build the agent graph of the running agents, with the lifecycle states placed as in the lanes view */
|
|
21
|
+
export const flowOf = (agents, groups) => {
|
|
22
|
+
const spots = new Map();
|
|
23
|
+
groups.forEach((g, col) => g.lanes.forEach((lane, l) => spots.set(lane.status, { col, row: lane.active ? 2 : l === 0 ? 1 : 3 })));
|
|
24
|
+
const order = groups.flatMap((g) => g.lanes.map((lane) => lane.status));
|
|
25
|
+
/* merge the agents of the same kind and mode into one card */
|
|
26
|
+
const merged = new Map();
|
|
27
|
+
for (const agent of [...agents].sort((a, b) => a.started - b.started)) {
|
|
28
|
+
const id = `${CARD}${agent.kind}:${agent.mode}`;
|
|
29
|
+
if (!merged.has(id))
|
|
30
|
+
merged.set(id, []);
|
|
31
|
+
merged.get(id).push(agent);
|
|
32
|
+
}
|
|
33
|
+
const rank = (state) => state === undefined || !order.includes(state) ? order.length : order.indexOf(state);
|
|
34
|
+
const cards = new Map();
|
|
35
|
+
const list = [...merged.entries()].map(([id, group]) => {
|
|
36
|
+
const first = group[0];
|
|
37
|
+
return {
|
|
38
|
+
id, agents: group, name: first.name, kind: first.kind, mode: first.mode,
|
|
39
|
+
busy: group.some((a) => a.state === "busy"),
|
|
40
|
+
tasks: [...new Set(group.flatMap((a) => a.tasks))].sort(TaskFormat.compareIds),
|
|
41
|
+
...(first.states !== undefined ? { states: first.states } : {})
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
/* order the cards along the lifecycle (by their activity or first incoming state),
|
|
45
|
+
with the request agents (without any states) last */
|
|
46
|
+
list.sort((a, b) => rank(a.states?.activity ?? a.states?.input[0]) - rank(b.states?.activity ?? b.states?.input[0])
|
|
47
|
+
|| a.kind.localeCompare(b.kind) || a.mode.localeCompare(b.mode));
|
|
48
|
+
for (const card of list)
|
|
49
|
+
cards.set(card.id, card);
|
|
50
|
+
/* the referenced states (in lifecycle order) and the edges, where a gate state
|
|
51
|
+
which is also an incoming state of the agent yields no additional edge */
|
|
52
|
+
const states = new Set();
|
|
53
|
+
const edges = [];
|
|
54
|
+
for (const card of list) {
|
|
55
|
+
const s = card.states;
|
|
56
|
+
if (s === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
for (const state of s.input) {
|
|
59
|
+
states.add(state);
|
|
60
|
+
edges.push({ from: STATE + state, to: card.id });
|
|
61
|
+
}
|
|
62
|
+
if (s.gate !== undefined && !s.input.includes(s.gate)) {
|
|
63
|
+
states.add(s.gate);
|
|
64
|
+
edges.push({ from: STATE + s.gate, to: card.id, dashed: true });
|
|
65
|
+
}
|
|
66
|
+
for (const state of s.output) {
|
|
67
|
+
states.add(state);
|
|
68
|
+
edges.push({ from: card.id, to: STATE + state });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { cards, states: [...states].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)), edges, spots };
|
|
72
|
+
};
|
|
73
|
+
/* the label of the container of an activity state */
|
|
74
|
+
const containerLabel = (state) => `◉ ${state}`;
|
|
75
|
+
/* lay out the agent graph top to bottom in rows: the request agents on top, below them
|
|
76
|
+
the lifecycle states in the grid of the lanes view (the entry states, the active states
|
|
77
|
+
as the containers nesting the cards of the agents operating in them, plus the cards
|
|
78
|
+
of the unclaimed phase agents, and the parking states), with the phases as columns */
|
|
79
|
+
export const layoutFlow = (flow, unit) => {
|
|
80
|
+
const u = units[unit];
|
|
81
|
+
const labels = new Map();
|
|
82
|
+
const rows = new Map();
|
|
83
|
+
const spot = (state) => flow.spots.get(state ?? "") ?? { col: Number.MAX_SAFE_INTEGER, row: 1 };
|
|
84
|
+
/* the common width of all state nodes: wide enough for the longest state name
|
|
85
|
+
and for the most ports on one side of a state node (as else the layout widens
|
|
86
|
+
each state node individually for its own ports) */
|
|
87
|
+
const ports = (state) => Math.max(flow.edges.filter((e) => e.to === STATE + state).length, flow.edges.filter((e) => e.from === STATE + state).length);
|
|
88
|
+
const stateW = Math.max(0, ...flow.states.map((state) => Math.max(unit === "cell" ? state.length + 4 : u.gw(state), (ports(state) + 1) * portSpacing[unit])));
|
|
89
|
+
const nodes = [
|
|
90
|
+
...flow.states.map((state) => {
|
|
91
|
+
const s = spot(state);
|
|
92
|
+
rows.set(STATE + state, s.row);
|
|
93
|
+
return { id: STATE + state, w: stateW, h: u.h, ...s };
|
|
94
|
+
}),
|
|
95
|
+
...[...flow.cards.values()].map((card) => {
|
|
96
|
+
const activity = card.states?.activity;
|
|
97
|
+
const box = { id: card.id, w: unit === "cell" ? CARD_W : CARD_PX_W, h: unit === "cell" ? CARD_H : CARD_PX_H };
|
|
98
|
+
if (card.states === undefined) {
|
|
99
|
+
rows.set(card.id, 0);
|
|
100
|
+
return { ...box, col: -1, row: 0 };
|
|
101
|
+
}
|
|
102
|
+
if (activity === undefined) {
|
|
103
|
+
rows.set(card.id, 2);
|
|
104
|
+
return { ...box, col: spot(card.states.input[0]).col, row: 2 };
|
|
105
|
+
}
|
|
106
|
+
labels.set(activity, containerLabel(activity));
|
|
107
|
+
rows.set(activity, 2);
|
|
108
|
+
return { ...box, group: activity, col: spot(activity).col, row: 2 };
|
|
109
|
+
})
|
|
110
|
+
];
|
|
111
|
+
/* the order of the nodes (and hence of the containers) within each row: by their columns */
|
|
112
|
+
nodes.sort((a, b) => a.col - b.col || a.row - b.row);
|
|
113
|
+
return layoutBoxes(unit, nodes.map(({ col: _col, row: _row, ...n }) => n), labels, flow.edges, rows);
|
|
114
|
+
};
|
|
115
|
+
/* whether a node id of the agent graph is an agent card (and not a state node),
|
|
116
|
+
and the node id of a state node */
|
|
117
|
+
export const isFlowCard = (id) => id.startsWith(CARD);
|
|
118
|
+
export const flowState = (state) => STATE + state;
|
|
119
|
+
/* the agent cards related to a state node of the agent graph: the ones observing it
|
|
120
|
+
(incoming), writing it (outgoing), and gating on it, plus its state */
|
|
121
|
+
export const flowRelations = (flow, id) => {
|
|
122
|
+
const state = id.slice(STATE.length);
|
|
123
|
+
const cards = [...flow.cards.values()];
|
|
124
|
+
return {
|
|
125
|
+
state,
|
|
126
|
+
input: cards.filter((c) => c.states?.input.includes(state) === true),
|
|
127
|
+
output: cards.filter((c) => c.states?.output.includes(state) === true),
|
|
128
|
+
gate: cards.filter((c) => c.states?.gate === state && !c.states.input.includes(state))
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/* the agent name of a card, with the number of its merged agents */
|
|
132
|
+
const cardName = (card) => card.agents.length > 1 ? `${card.name} ×${card.agents.length}` : card.name;
|
|
133
|
+
/* remove control characters from a text of the agent relay */
|
|
134
|
+
const clean = (text) => text.replace(/\p{Cc}/gu, "");
|
|
135
|
+
/* draw a cell-unit layout of the agent graph onto a character grid, returning per cell the
|
|
136
|
+
character and a tone for coloring (like the task graph): the state nodes in tone "done" (or "sel"), the
|
|
137
|
+
agent cards in tone "active" (busy), "idle" (waiting), or "sel" (selected, with a heavy border, like
|
|
138
|
+
a selected state), with the name of
|
|
139
|
+
the agent inverse on their first line, followed by their field lines (with the tone suffix
|
|
140
|
+
"-title" on the labels, "-value" on their bold values, and the busy task ids inverse), and the given pulse glyph (else a filled
|
|
141
|
+
circle) of a busy card, resp. an empty circle of a waiting one, in the right part of their top border */
|
|
142
|
+
export const drawFlowText = (flow, layout, selected, pulse = "", beat = () => false) => {
|
|
143
|
+
const g = textGrid(layout);
|
|
144
|
+
const { W, H, chars, tones } = g;
|
|
145
|
+
const put = (x, y, max, text, tone) => {
|
|
146
|
+
const n = y < H ? Math.min(text.length, max, W - x) : 0;
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
chars[y][x + i] = text[i];
|
|
149
|
+
tones[y][x + i] = tone;
|
|
150
|
+
}
|
|
151
|
+
return x + Math.max(0, n);
|
|
152
|
+
};
|
|
153
|
+
drawContainers(g, layout.groups, containerLabel);
|
|
154
|
+
drawEdges(g, layout.edges, selected, true);
|
|
155
|
+
drawContainerLabels(g, layout.groups, containerLabel);
|
|
156
|
+
for (const n of layout.nodes.values()) {
|
|
157
|
+
if (!isFlowCard(n.id)) {
|
|
158
|
+
/* the state centered within its box (which may be widened for its ports) */
|
|
159
|
+
const state = n.id.slice(STATE.length);
|
|
160
|
+
const tone = n.id === selected ? "sel" : "done";
|
|
161
|
+
drawBox(g, n, tone, false, n.id === selected);
|
|
162
|
+
put(n.x + Math.max(2, Math.floor((n.w - state.length) / 2)), n.y + 1, n.w - 4, state, `${tone}-title`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const card = flow.cards.get(n.id);
|
|
166
|
+
if (card === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
const base = card.busy ? "active" : "idle";
|
|
169
|
+
const tone = n.id === selected ? "sel" : base;
|
|
170
|
+
drawBox(g, n, tone, false, n.id === selected);
|
|
171
|
+
if (n.w > 6)
|
|
172
|
+
chars[n.y][n.x + n.w - 3] = !card.busy ? "○" : pulse !== "" ? pulse : "●";
|
|
173
|
+
/* the name line (with the heart of the agents in the top right corner inside the card,
|
|
174
|
+
like in the agents view, filled for a second after a heartbeat of any of them)
|
|
175
|
+
and the field lines, cut at the right border */
|
|
176
|
+
const max = n.w - 4;
|
|
177
|
+
const room = max - 2;
|
|
178
|
+
const name = ` ⚉ ${clean(cardName(card))} `;
|
|
179
|
+
put(n.x + 2, n.y + 1, room, name.length > room ? name.slice(0, room - 1) + "…" : name, `${tone}-id`);
|
|
180
|
+
put(n.x + n.w - 3, n.y + 1, 1, card.agents.some((a) => beat(a.id)) ? "♥" : "♡", tone);
|
|
181
|
+
const fields = [
|
|
182
|
+
["kind:", clean(card.kind)],
|
|
183
|
+
["mode:", card.mode !== "" ? clean(card.mode) : "none"],
|
|
184
|
+
["task:", ""]
|
|
185
|
+
];
|
|
186
|
+
fields.forEach(([label, value], k) => {
|
|
187
|
+
const y = n.y + 2 + k;
|
|
188
|
+
if (y >= n.y + n.h - 1)
|
|
189
|
+
return;
|
|
190
|
+
let x = put(n.x + 2, y, max, label.padEnd(6), `${tone}-title`);
|
|
191
|
+
const end = n.x + 2 + max;
|
|
192
|
+
if (label !== "task:")
|
|
193
|
+
put(x, y, end - x, value, `${tone}-value`);
|
|
194
|
+
else if (card.tasks.length === 0)
|
|
195
|
+
put(x, y, end - x, `none (${card.agents[0].state})`, `${tone}-value`);
|
|
196
|
+
else
|
|
197
|
+
for (const id of card.tasks) {
|
|
198
|
+
if (x >= end)
|
|
199
|
+
break;
|
|
200
|
+
x = put(x, y, end - x, ` ≡ ${clean(id)} `, `${tone}-id`) + 1;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return { lines: chars.map((r) => r.join("")), tones };
|
|
205
|
+
};
|
|
206
|
+
/* draw a pixel-unit layout of the agent graph as SVG, with every activity container labeled
|
|
207
|
+
with its state, every state node a "g.node.state" box with its state, and every agent card a
|
|
208
|
+
"g.node" holding the same HTML card as the agents view (with the class "busy" for a busy agent,
|
|
209
|
+
and with the heart of its agents, carrying their ids, to be filled by the page on their heartbeats),
|
|
210
|
+
both carrying their "data-id" (for the click delegation of the page) */
|
|
211
|
+
export const drawFlowSVG = (flow, layout) => {
|
|
212
|
+
const out = drawSVGFrame("flow", layout, containerLabel, true);
|
|
213
|
+
for (const n of layout.nodes.values()) {
|
|
214
|
+
if (!isFlowCard(n.id)) {
|
|
215
|
+
out.push(`<g class="node state" data-id="${escapeXML(n.id)}"><rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="5"/>` +
|
|
216
|
+
`<text x="${n.x + n.w / 2}" y="${n.y + n.h / 2}">${escapeXML(n.id.slice(STATE.length))}</text></g>`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const card = flow.cards.get(n.id);
|
|
220
|
+
if (card === undefined)
|
|
221
|
+
continue;
|
|
222
|
+
const field = (label, value) => `<span>${label}</span><b class="aval">${escapeXML(clean(value))}</b>`;
|
|
223
|
+
const tasks = card.tasks.length === 0 ? `<b>none (${escapeXML(card.agents[0].state)})</b>` :
|
|
224
|
+
card.tasks.map((id) => `<span class="atask">≡ ${escapeXML(clean(id))}</span>`).join("");
|
|
225
|
+
out.push(`<g class="node" data-id="${escapeXML(card.id)}">` +
|
|
226
|
+
`<foreignObject x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}">` +
|
|
227
|
+
`<div xmlns="http://www.w3.org/1999/xhtml" class="acard${card.busy ? " busy" : ""}">` +
|
|
228
|
+
`<div class="aname"><span class="abadge"><span class="aicon">⚉</span> ${escapeXML(clean(cardName(card)))}</span></div>` +
|
|
229
|
+
`<span class="abeat" data-agents="${escapeXML(card.agents.map((a) => a.id).join(" "))}">♡</span>` +
|
|
230
|
+
"<div class=\"afields\">" +
|
|
231
|
+
field("kind:", card.kind) + field("mode:", card.mode !== "" ? card.mode : "none") +
|
|
232
|
+
`<span>task:</span><span class="aval atasks">${tasks}</span>` +
|
|
233
|
+
"</div></div></foreignObject></g>");
|
|
234
|
+
}
|
|
235
|
+
out.push("</svg>");
|
|
236
|
+
return out.join("\n");
|
|
237
|
+
};
|