@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,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
+ };