@rse/ase 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -0,0 +1,272 @@
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 crypto from "node:crypto";
7
+ import os from "node:os";
8
+ import { Task } from "./ase-task.js";
9
+ import * as TaskFormat from "./ase-task-format.js";
10
+ import { Agent, updateTask, tagsOf, flatKeys } from "./ase-agent-runtime.js";
11
+ import { skillErrored } from "./ase-agent-runtime-tool.js";
12
+ import { runOwner } from "./ase-agent-runtime-claim.js";
13
+ import { typeParam, attachmentTime, requestRefused, requestDeferred } from "./ase-task-request.js";
14
+ /* the frontmatter keys of a task plan which are no plan content, as they are
15
+ changed by the agents and the task boards (and not by the editing of the plan),
16
+ or maintained by the task stores on every save (like the modification time,
17
+ which the issue tracker stores even derive from the last update of the issue) */
18
+ const nonContentKeys = ["Status", "Tags", "Assignee", "Modified"];
19
+ /* the agent processing the requests attached to tasks (see "TaskRequests") in any task
20
+ lifecycle model, independent of the status of the tasks (except for refusing the requests
21
+ of finished tasks and deferring those of tasks in an activity status): it claims the oldest
22
+ request of a task by turning it into a processed one and assigning the task to the agent
23
+ (keeping all other agents off it), lets the concrete agent process the prompt of the request
24
+ via an ASE skill of the headless agent tool, restores the previous assignee, and removes the
25
+ request if the task plan was changed (else turns it into a failed one, or back into a
26
+ requested one on interrupt), where a per-process run lock (in the per-user state directory,
27
+ hence per host and user, see "AgentRun") lets it recover the requests left behind by
28
+ terminated agents of its kind */
29
+ export class RequestAgent extends Agent {
30
+ lifecycles = ["solo", "team", "enterprise"];
31
+ /* the claimed requests per task id: their prompt, the previous assignee
32
+ of the task, and the digest of the plan content right after the claim */
33
+ claims = new Map();
34
+ /* the assignee under which the agent claims, holds, recognizes, and releases
35
+ the tasks (default: its name) */
36
+ get assignee() {
37
+ return this.name;
38
+ }
39
+ /* decide on the assignee to hand a processed task <task> over to, by its success
40
+ and its previous assignee <previous> before the claim (or empty to unassign it):
41
+ by default, the previous assignee is restored */
42
+ handover(_success, previous, _task) {
43
+ return previous;
44
+ }
45
+ /* whether an attachment type is the one of a request processed by this agent process */
46
+ own(type) {
47
+ return this.requests.state(type) === "active" && typeParam(type, "run") === this.runLock.id;
48
+ }
49
+ /* whether an attachment type is the one of a request processed by an agent process
50
+ whose run lock is in the same per-user state directory on this host (by its host and
51
+ owner), i.e., whose termination can be determined (by its run lock and its process id) */
52
+ local(type) {
53
+ return this.requests.state(type) === "active"
54
+ && typeParam(type, "host") === os.hostname() && typeParam(type, "owner") === runOwner;
55
+ }
56
+ /* the digest of the content of a task plan, as the processing can change it: its
57
+ frontmatter (except the keys changed by the agents and the task boards, like its
58
+ status, tags, and assignee) and its body, but none of its attachments (like the
59
+ requests and the agent processing logs), for detecting whether it was edited */
60
+ digest(plan) {
61
+ const header = Object.fromEntries(Object.entries(plan.header)
62
+ .filter(([key]) => !nonContentKeys.includes(key)));
63
+ return crypto.createHash("sha1").update(JSON.stringify({ header, body: plan.body })).digest("hex");
64
+ }
65
+ /* the tag "ase-agent-<kind>:active" of a phase agent holding a task (else empty), which
66
+ it carries from the claim until the settling by the phase agent, i.e., also while the
67
+ phase agent still finishes a task already moved into its "success" state by the skill
68
+ (e.g. commits and publishes its change set), so the task is still neither to be
69
+ assigned to this agent nor to be edited meanwhile */
70
+ held(keys) {
71
+ return tagsOf(keys).find((tag) => /^ase-agent-[^:]+:active$/.test(tag)
72
+ && !tag.startsWith(`${this.requests.tag}:`)) ?? "";
73
+ }
74
+ /* recover the requests of terminated agent processes of its kind of the same user
75
+ on this host (whose run lock is gone or stale and whose process is gone) by marking
76
+ them as failed and restoring the previous assignee of their tasks, and remove the
77
+ run locks left behind by them */
78
+ async recover() {
79
+ const runs = await this.runLock.dead();
80
+ const { items } = await Task.listHeaders(this.log);
81
+ for (const item of items.filter((item) => this.requests.of(item.keys) === "active")) {
82
+ const dead = new Set();
83
+ for (const att of await Task.attachments(this.log, item.id)) {
84
+ const run = typeParam(att.type, "run");
85
+ if (this.local(att.type) && !(await this.runLock.alive(run, Number.parseInt(typeParam(att.type, "pid"), 10) || 0)))
86
+ dead.add(run);
87
+ }
88
+ if (dead.size === 0)
89
+ continue;
90
+ await updateTask(this.log, item.id, (plan) => {
91
+ let changed = false;
92
+ for (let i = 0; i < plan.attachment.length; i++) {
93
+ const type = plan.attachment[i].Type ?? "";
94
+ if (this.local(type) && dead.has(typeParam(type, "run"))) {
95
+ this.requests.finalize(plan, i, "failed", `failed (${this.role} terminated during processing)`);
96
+ changed = true;
97
+ }
98
+ }
99
+ return changed;
100
+ });
101
+ await this.write("warning", this.name, `task "${item.id}": ${this.requests.name} request of terminated ${this.role} marked as failed`);
102
+ }
103
+ for (const { run } of runs)
104
+ await this.runLock.purge(run);
105
+ }
106
+ /* select every task with a request (except one whose finalization is still retried),
107
+ deferring those in an activity status or still held by a phase agent */
108
+ select(items, busy) {
109
+ const lifecycle = this.lifecycle;
110
+ const tasks = [];
111
+ const waiting = new Map();
112
+ for (const item of items.filter((item) => this.requests.of(item.keys) === "pending" && !busy.has(item.id) && !this.unsettled.has(item.id))) {
113
+ const held = this.held(item.keys);
114
+ if (requestDeferred(lifecycle, item.status))
115
+ waiting.set(item.id, `its status "${item.status}" to change`);
116
+ else if (held !== "")
117
+ waiting.set(item.id, `its tag "${held}" to be released`);
118
+ else
119
+ tasks.push({ id: item.id, title: item.title, keys: item.keys });
120
+ }
121
+ return { tasks, waiting };
122
+ }
123
+ /* claim the oldest request of a task, in a single conditional update of the
124
+ task (so a concurrent claim, e.g. by a phase agent, makes it fail): turn the request
125
+ into a processed one and assign the task to the agent (remembering its previous
126
+ assignee), which keeps all other agents off the task during its processing, where the
127
+ request of a finished task is marked as failed instead, and a task meanwhile in an
128
+ activity status or meanwhile held by a phase agent is skipped, and a request without
129
+ a prompt is marked as failed (unless the agent ignores the prompts of its requests) */
130
+ async claim(task) {
131
+ const lifecycle = this.lifecycle;
132
+ let status = "";
133
+ let prompt = "";
134
+ let assignee = "";
135
+ let sum = "";
136
+ let refused = "";
137
+ let claimed = false;
138
+ await updateTask(this.log, task.id, (plan) => {
139
+ status = TaskFormat.taskStatus(plan.header, lifecycle);
140
+ prompt = "";
141
+ refused = "";
142
+ claimed = false;
143
+ /* adopt a request already claimed by this run, as found by an update
144
+ retried after a saved claim whose response was lost */
145
+ const own = plan.attachment.find((att) => this.own(att.Type ?? ""));
146
+ if (own !== undefined) {
147
+ prompt = (own.Data ?? "").trim();
148
+ assignee = typeParam(own.Type ?? "", "assignee");
149
+ sum = this.digest(plan);
150
+ claimed = true;
151
+ return false;
152
+ }
153
+ const i = plan.attachment.findIndex((att) => this.requests.state(att.Type ?? "") === "pending");
154
+ if (i < 0 || requestDeferred(lifecycle, status) || this.requests.running(plan)
155
+ || this.held(flatKeys(plan.header)) !== "")
156
+ return false;
157
+ const att = plan.attachment[i];
158
+ if (requestRefused(lifecycle, status))
159
+ refused = `task is finished, status "${status}"`;
160
+ else if ((att.Data ?? "").trim() === "" && this.presencePrompt === "required")
161
+ refused = "empty prompt";
162
+ if (refused !== "")
163
+ plan.attachment[i] = { ...att, Type: this.requests.type("failed"), Desc: `failed (${refused})`, Modified: attachmentTime() };
164
+ else {
165
+ prompt = (att.Data ?? "").trim();
166
+ assignee = String(plan.header.Assignee ?? "");
167
+ claimed = true;
168
+ const params = {
169
+ agent: this.assignee, assignee, host: os.hostname(), owner: runOwner,
170
+ pid: String(process.pid), run: this.runLock.id
171
+ };
172
+ plan.attachment[i] = { ...att, Type: this.requests.type("active", params), Modified: attachmentTime() };
173
+ plan.header.Assignee = this.assignee;
174
+ }
175
+ this.requests.retag(plan);
176
+ sum = this.digest(plan);
177
+ return true;
178
+ });
179
+ if (refused !== "") {
180
+ await this.write("warning", this.name, `task "${task.id}": ${this.requests.name} request refused (${refused})`);
181
+ return null;
182
+ }
183
+ else if (!claimed)
184
+ return null;
185
+ this.claims.set(task.id, { prompt, assignee, digest: sum });
186
+ return `claimed ${this.requests.name} request (status "${status}", assigned to "${this.assignee}"` +
187
+ (assignee !== "" ? ` instead of "${assignee}")` : ")");
188
+ }
189
+ /* decide whether the processing of a request failed, returning the reason of the failure,
190
+ or null on success (called within the settling update of the task, with its current plan,
191
+ so it must neither change the plan nor block): by default, the processing succeeded only if
192
+ the plan content changed since the claim and the skill reported no error (independent of the
193
+ exit code of the agent tool and an interrupt, as the plan is saved at once), where a concrete
194
+ agent whose processing succeeds without changing the plan (e.g. one producing an attachment
195
+ or an external result only) overrides this with its own success criterion */
196
+ failure(outcome) {
197
+ const { changed, errored, ok } = outcome;
198
+ return changed && !errored ? null : errored ? "skill reported an error" : !ok ? "agent tool failed" : "plan not changed";
199
+ }
200
+ /* settle the processed request in a single update of the task: if the processing succeeded
201
+ (see "failure"), the request is removed, else it is turned back into a requested one on
202
+ interrupt, or into a failed one otherwise, and in all cases the previous assignee of the task
203
+ is restored (or the task is handed over, see "handover"); a finalization failing even after
204
+ its retries is retried in the background
205
+ (see "unsettle"), so the request never stays processed */
206
+ async settle(task, ok, _finished, aborted, output, log) {
207
+ const claim = this.claims.get(task.id);
208
+ this.claims.delete(task.id);
209
+ if (claim === undefined)
210
+ return;
211
+ const errored = skillErrored(output);
212
+ let result = "";
213
+ let success = false;
214
+ const finish = () => updateTask(this.log, task.id, (plan) => {
215
+ success = false;
216
+ const i = plan.attachment.findIndex((att) => this.own(att.Type ?? ""));
217
+ if (i < 0) {
218
+ /* the request vanished meanwhile: just restore the assignee and the tag */
219
+ const before = JSON.stringify([plan.header.Assignee, plan.header.Tags]);
220
+ if (String(plan.header.Assignee ?? "") === this.assignee) {
221
+ if (claim.assignee !== "")
222
+ plan.header.Assignee = claim.assignee;
223
+ else
224
+ delete plan.header.Assignee;
225
+ }
226
+ this.requests.retag(plan);
227
+ result = `${this.requests.name} request vanished meanwhile`;
228
+ return JSON.stringify([plan.header.Assignee, plan.header.Tags]) !== before;
229
+ }
230
+ const changed = this.digest(plan) !== claim.digest;
231
+ const reason = this.failure({ id: task.id, plan, changed, errored, ok, aborted, output });
232
+ if (reason === null) {
233
+ this.requests.finalize(plan, i, null, undefined, this.handover(true, claim.assignee, task));
234
+ result = `task ${this.requests.done} successfully (request removed)`;
235
+ success = true;
236
+ }
237
+ else if (aborted) {
238
+ this.requests.finalize(plan, i, "pending");
239
+ result = `${this.requests.name} request released (requested again)`;
240
+ }
241
+ else {
242
+ this.requests.finalize(plan, i, "failed", `failed (${reason}, see attachment "agent")`, this.handover(false, claim.assignee, task));
243
+ result = `task not ${this.requests.done} (${reason}, request marked as failed)`;
244
+ }
245
+ return true;
246
+ });
247
+ let exists;
248
+ try {
249
+ exists = await finish();
250
+ }
251
+ catch (err) {
252
+ /* the retried finalization reports its result itself (as the processing log of
253
+ the task is already closed), or the vanishing of the task, which ends the retries */
254
+ this.unsettle(task.id, async () => {
255
+ const exists = await finish();
256
+ await this.write(exists ? (success ? "info" : "warning") : "warning", `${this.name}: ${task.id}`, exists ? `${result} (finalization retried)` : "task vanished -- finalization of the request dropped", undefined, task.id);
257
+ return true;
258
+ });
259
+ await log("error", `finalizing the ${this.requests.name} request failed (retried later): ` +
260
+ `${err instanceof Error ? err.message : String(err)}`);
261
+ return;
262
+ }
263
+ if (exists)
264
+ await log(success ? "info" : "warning", result);
265
+ else
266
+ await log("warning", "task vanished meanwhile");
267
+ }
268
+ /* the description of the observed tasks (for the start log line) */
269
+ observing() {
270
+ return `tasks tagged with "${this.requests.tag}:pending"`;
271
+ }
272
+ }
@@ -0,0 +1,207 @@
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 { stripVTControlCharacters } from "node:util";
7
+ /* the maximum number of lines of an agent message kept in the processing log attachment
8
+ of the task (as an agent message can be huge, e.g. the rendering of the whole task plan,
9
+ which would otherwise be fed back into the next rendering of the task plan) */
10
+ const TEXT_MAX = 20;
11
+ /* the per-tool headless invocation of the rendered prompt <prompt> with access to the
12
+ additional directories <add> (the "--add-dir" options), streaming JSONL events,
13
+ where risky actions are denied or automatically reviewed instead of being prompted for
14
+ (the variadic "--add-dir" of Claude has to precede the other options to not consume the prompt) */
15
+ export const toolArgs = {
16
+ "claude": (prompt, add) => [
17
+ ...add, "-p", "--output-format", "stream-json", "--verbose", "--disallowedTools", "AskUserQuestion",
18
+ "--permission-mode", "auto", "--permission-prompts", "none", prompt
19
+ ],
20
+ "copilot": (prompt, add) => [
21
+ ...add, "--allow-all-tools", "--no-ask-user", "--no-color", "--output-format", "json", "-p", prompt
22
+ ],
23
+ "codex": (prompt, add) => [
24
+ "exec", ...add, "--json", "--approve-for-me", "--color", "never", prompt
25
+ ]
26
+ };
27
+ /* the per-tool rendering of the invocation of the skill <skill> with the arguments <args>:
28
+ "/<skill>" for Claude and Copilot and "$<skill>" for Codex, where an un-namespaced ASE
29
+ skill ("ase-*") gets the plugin namespace "ase:" for Claude only (as Copilot and Codex
30
+ invoke the plugin skills un-namespaced), and the arguments are appended with a single
31
+ space only if non-empty */
32
+ export const skillPrompt = (tool, skill, args) => {
33
+ const name = tool === "claude" && skill.startsWith("ase-") && !skill.includes(":") ? `ase:${skill}` : skill;
34
+ return `${tool === "codex" ? "$" : "/"}${name}${args !== "" ? ` ${args}` : ""}`;
35
+ };
36
+ /* abbreviate a value into a single line of at most <max> characters */
37
+ const brief = (value, max = 160) => {
38
+ const line = (typeof value === "string" ? value : JSON.stringify(value) ?? "").replace(/\s+/g, " ").trim();
39
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
40
+ };
41
+ /* the constructors of processing log entries: informational, warning,
42
+ warning on failure (else informational), and agent message text */
43
+ const info = (msg) => ({ level: "info", msg });
44
+ const warn = (msg) => ({ level: "warning", msg });
45
+ const note = (failed, msg) => failed ? warn(msg) : info(msg);
46
+ const text = (msg) => ({ level: "info", msg, text: true });
47
+ /* the per-tool decoding of a JSONL event of the agent tool into processing log entries,
48
+ covering agent messages, tool calls, failures, and the final result only */
49
+ const toolEvents = {
50
+ "claude": (ev) => {
51
+ const content = Array.isArray(ev.message?.content) ? ev.message.content : [];
52
+ if (ev.type === "system" && ev.subtype === "init")
53
+ return [info(`session ${ev.session_id} started (model: ${ev.model})`)];
54
+ else if (ev.type === "system" && ev.subtype === "api_retry")
55
+ return [warn(`API retry ${ev.attempt}/${ev.max_retries} (error: ${ev.error})`)];
56
+ else if (ev.type === "assistant")
57
+ return content.filter((block) => block.type === "text" || block.type === "tool_use")
58
+ .map((block) => block.type === "text" ? text(block.text) : info(`tool ${block.name}: ${brief(block.input)}`));
59
+ else if (ev.type === "user")
60
+ return content.filter((block) => block.type === "tool_result" && block.is_error === true)
61
+ .map((block) => warn(`tool failed: ${brief(block.content)}`));
62
+ else if (ev.type === "result")
63
+ return [
64
+ ...(Array.isArray(ev.permission_denials) ? ev.permission_denials : [])
65
+ .map((denial) => warn(`permission denied: tool ${denial.tool_name}: ${brief(denial.tool_input)}`)),
66
+ note(ev.is_error === true, `result: ${ev.subtype} (turns: ${ev.num_turns}, ` +
67
+ `duration: ${Math.round((ev.duration_ms ?? 0) / 1000)}s, cost: $${Number(ev.total_cost_usd ?? 0).toFixed(2)})`)
68
+ ];
69
+ return [];
70
+ },
71
+ "copilot": (ev) => {
72
+ const data = ev.data ?? {};
73
+ if (ev.type === "session.start")
74
+ return [info(`session ${data.sessionId} started (model: ${data.selectedModel ?? "default"})`)];
75
+ else if (ev.type === "assistant.message" && typeof data.content === "string" && data.content !== "")
76
+ return [text(data.content)];
77
+ else if (ev.type === "tool.execution_start")
78
+ return [info(`tool ${data.toolName}: ${brief(data.arguments ?? {})}`)];
79
+ else if (ev.type === "tool.execution_complete" && data.success !== true)
80
+ return [warn(`tool failed: ${brief(data.error?.message ?? data.result?.content ?? "")}`)];
81
+ else if (ev.type === "subagent.failed")
82
+ return [warn(`sub-agent ${data.agentName} failed: ${brief(data.error)}`)];
83
+ else if (ev.type === "session.error" || ev.type === "session.warning")
84
+ return [warn(`${ev.type.replace(/^session\./, "")}: ${brief(data.message)}`)];
85
+ else if (ev.type === "result")
86
+ return [note(ev.exitCode !== 0, `result: exit code ${ev.exitCode} ` +
87
+ `(duration: ${Math.round((ev.usage?.sessionDurationMs ?? 0) / 1000)}s, ` +
88
+ `premium requests: ${ev.usage?.premiumRequests ?? 0})`)];
89
+ return [];
90
+ },
91
+ "codex": (ev) => {
92
+ const item = ev.item ?? {};
93
+ const done = ev.type === "item.completed";
94
+ if (ev.type === "thread.started")
95
+ return [info(`thread ${ev.thread_id} started`)];
96
+ else if (ev.type === "item.started" && item.type === "command_execution")
97
+ return [info(`command: ${brief(item.command)}`)];
98
+ else if (ev.type === "item.started" && item.type === "mcp_tool_call")
99
+ return [info(`tool ${item.server}/${item.tool}: ${brief(item.arguments ?? {})}`)];
100
+ else if (done && item.type === "agent_message")
101
+ return [text(item.text)];
102
+ else if (done && item.type === "command_execution" && item.status !== "completed")
103
+ return [warn(`command ${item.status} (exit code ${item.exit_code ?? "none"}): ${brief(item.command)}`)];
104
+ else if (done && item.type === "mcp_tool_call" && item.status !== "completed")
105
+ return [warn(`tool ${item.server}/${item.tool} failed: ${brief(item.error?.message ?? "")}`)];
106
+ else if (done && item.type === "file_change")
107
+ return [note(item.status !== "completed", `file change ${item.status}: ` +
108
+ (Array.isArray(item.changes) ? item.changes : []).map((change) => `${change.kind} ${change.path}`).join(", "))];
109
+ else if (done && item.type === "web_search")
110
+ return [info(`web search: ${brief(item.query)}`)];
111
+ else if (done && item.type === "error")
112
+ return [warn(`error: ${brief(item.message)}`)];
113
+ else if (ev.type === "turn.completed")
114
+ return [info(`turn completed (tokens: input ${ev.usage?.input_tokens ?? 0}, output ${ev.usage?.output_tokens ?? 0})`)];
115
+ else if (ev.type === "turn.failed")
116
+ return [warn(`turn failed: ${brief(ev.error?.message)}`)];
117
+ else if (ev.type === "error")
118
+ return [warn(`error: ${brief(ev.message)}`)];
119
+ return [];
120
+ }
121
+ };
122
+ /* a reported amount (where a missing or invalid one counts as zero) */
123
+ const amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
124
+ /* the per-tool accounting of a JSONL event of the agent tool into its costs: the output tokens
125
+ (as the input tokens are dominated by the context re-sent on every turn) and the costs in USD
126
+ of Claude (over all its models, i.e. including its sub-agents, from its final result), nothing
127
+ of Copilot (as it reports premium requests only), and the output tokens of Codex (summed over
128
+ its turns), plus the cache and input tokens and the AI request time where reported (where
129
+ the input tokens of Claude exclude the cache tokens, while those of Codex include them) */
130
+ const toolCosts = {
131
+ "claude": (ev, costs) => {
132
+ if (ev.type !== "result")
133
+ return;
134
+ const models = ev.modelUsage !== null && typeof ev.modelUsage === "object" ? Object.values(ev.modelUsage) : [];
135
+ const sum = (key, fallback) => models.length > 0 ?
136
+ models.reduce((acc, u) => acc + amount(u?.[key]), 0) :
137
+ amount(ev.usage?.[fallback]);
138
+ costs.costToken = sum("outputTokens", "output_tokens");
139
+ costs.cread = sum("cacheReadInputTokens", "cache_read_input_tokens");
140
+ costs.cwrite = sum("cacheCreationInputTokens", "cache_creation_input_tokens");
141
+ costs.input = sum("inputTokens", "input_tokens");
142
+ costs.costUSD = amount(ev.total_cost_usd);
143
+ costs.apiMs = typeof ev.duration_api_ms === "number" ? amount(ev.duration_api_ms) : null;
144
+ },
145
+ "copilot": (ev, costs) => {
146
+ /* no tokens and costs reported */
147
+ if (ev.type === "result" && ev.usage?.sessionDurationMs !== undefined)
148
+ costs.apiMs = amount(ev.usage.sessionDurationMs);
149
+ },
150
+ "codex": (ev, costs) => {
151
+ if (ev.type === "turn.completed") {
152
+ const cread = amount(ev.usage?.cached_input_tokens);
153
+ const cwrite = amount(ev.usage?.cache_write_input_tokens);
154
+ costs.costToken = (costs.costToken ?? 0) + amount(ev.usage?.output_tokens);
155
+ costs.cread += cread;
156
+ costs.cwrite += cwrite;
157
+ costs.input += Math.max(0, amount(ev.usage?.input_tokens) - cread - cwrite);
158
+ }
159
+ }
160
+ };
161
+ /* decode an output line of the agent tool into processing log entries without
162
+ terminal control sequences: a JSONL event via the decoder of the tool (splitting
163
+ agent message text into its lines, and accounting it into the costs), and any
164
+ other non-blank line as plain text */
165
+ export const toolEntries = (tool, line, costs) => {
166
+ let ev = null;
167
+ if (line.startsWith("{")) {
168
+ try {
169
+ ev = JSON.parse(line);
170
+ }
171
+ catch (_e) {
172
+ /* not a JSONL event: treat as plain text */
173
+ }
174
+ }
175
+ if (ev === null || typeof ev !== "object") {
176
+ const plain = stripVTControlCharacters(line);
177
+ return plain.trim() !== "" ? [text(plain)] : [];
178
+ }
179
+ toolCosts[tool](ev, costs);
180
+ return toolEvents[tool](ev).flatMap((entry) => {
181
+ if (entry.text !== true)
182
+ return [{ ...entry, msg: stripVTControlCharacters(entry.msg) }];
183
+ const msgs = stripVTControlCharacters(entry.msg).replace(/\n+$/, "").split("\n");
184
+ return [
185
+ ...msgs.map((msg, i) => ({ ...text(msg), ...(i >= TEXT_MAX ? { quiet: true } : {}) })),
186
+ ...(msgs.length > TEXT_MAX ? [info(`(${msgs.length - TEXT_MAX} further lines of agent message ` +
187
+ "not kept in the processing log of the task)")] : [])
188
+ ];
189
+ });
190
+ };
191
+ /* whether a skill reported an error in the output lines of the headless agent tool, as the
192
+ skills report their errors as "▶ ERROR: ..." (see "ase-common-task") while the agent tool
193
+ still exits successfully, where an agent message merely quoting "ERROR:" (e.g. of some
194
+ code or log) is no error */
195
+ export const skillErrored = (output) => output.some((line) => /(?:^|▶ )ERROR:/.test(line.trim()));
196
+ /* the last verdict word following the label <label> in the output lines of a skill
197
+ (e.g. "APPROVE" of "**REVIEW VERDICT**: **APPROVE**"), or empty if none */
198
+ export const verdictOf = (output, label) => {
199
+ const re = new RegExp(`${label}\\W*([A-Z]+)`);
200
+ let verdict = "";
201
+ for (const line of output) {
202
+ const m = re.exec(line);
203
+ if (m !== null)
204
+ verdict = m[1];
205
+ }
206
+ return verdict;
207
+ };