@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,827 @@
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 fs from "node:fs";
7
+ import path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import YAML from "yaml";
10
+ import { projectRoot, userConfigDir } from "./ase-config-scope.js";
11
+ import * as TaskFormat from "./ase-task-format.js";
12
+ import { requestsOf, typeParam } from "./ase-task-request.js";
13
+ import { statePrevious } from "./ase-agent-runtime-phase.js";
14
+ import rebooter from "./ase-agent-definition-builtin/rebooter.js";
15
+ import condenser from "./ase-agent-definition-builtin/condenser.js";
16
+ import implementer from "./ase-agent-definition-builtin/implementer.js";
17
+ import qualifier from "./ase-agent-definition-builtin/qualifier.js";
18
+ import integrator from "./ase-agent-definition-builtin/integrator.js";
19
+ import editor from "./ase-agent-definition-builtin/editor.js";
20
+ import estimator from "./ase-agent-definition-builtin/estimator.js";
21
+ import researcher from "./ase-agent-definition-builtin/researcher.js";
22
+ /* the built-in agent kinds */
23
+ export const builtinKinds = [
24
+ rebooter, condenser, implementer, qualifier, integrator, editor, estimator, researcher
25
+ ];
26
+ /* the file name extensions of the definition files, by descending precedence */
27
+ const definitionExts = [".yaml", ".yml", ".ts", ".mts", ".js", ".mjs", ".cjs"];
28
+ /* discover the definition agent kinds of the definition files "<kind>.<ext>" of the user
29
+ ("<user-config-dir>/agents/") and of the project (".ase/agents/"), where a kind of the
30
+ project shadows one of the user, and both shadow a built-in agent kind, while a file of
31
+ an invalid or a <reserved> kind name is skipped with a warning (as are the further files
32
+ of an already discovered kind name in the same directory) */
33
+ export const discoverKinds = (log, reserved) => {
34
+ const root = projectRoot();
35
+ const dirs = [
36
+ { scope: "user", dir: path.join(userConfigDir(), "agents") },
37
+ ...(root !== null ? [{ scope: "project", dir: path.join(root, ".ase", "agents") }] : [])
38
+ ];
39
+ const kinds = new Map();
40
+ for (const { scope, dir } of dirs) {
41
+ let entries;
42
+ try {
43
+ entries = fs.readdirSync(dir);
44
+ }
45
+ catch (_err) {
46
+ continue;
47
+ }
48
+ const seen = new Set();
49
+ const rank = (entry) => definitionExts.indexOf(path.extname(entry).toLowerCase());
50
+ for (const entry of entries.filter((entry) => rank(entry) >= 0).sort((a, b) => rank(a) - rank(b))) {
51
+ const kind = path.basename(entry, path.extname(entry));
52
+ const file = path.join(dir, entry);
53
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(kind) || kind.length > 64)
54
+ log.write("warning", `agent: definition "${file}": skipped, as "${kind}" is no valid agent kind name ` +
55
+ "(expected up to 64 letters, digits, underscores, and dashes, starting with a letter or digit)");
56
+ else if (reserved.includes(kind))
57
+ log.write("warning", `agent: definition "${file}": skipped, as "${kind}" clashes with the command "ase agent ${kind}"`);
58
+ else if (seen.has(kind))
59
+ log.write("warning", `agent: definition "${file}": skipped, as "${kinds.get(kind).file}" ` +
60
+ `already defines the agent kind "${kind}"`);
61
+ else {
62
+ seen.add(kind);
63
+ kinds.set(kind, { kind, file, scope });
64
+ }
65
+ }
66
+ }
67
+ return [...kinds.values()];
68
+ };
69
+ /* the processing types, the working directory kinds, and the success criteria of a definition */
70
+ export const definitionTypes = ["phase", "request"];
71
+ export const definitionWorkdirs = ["project", "source", "changeset", "changeset-read", "target"];
72
+ export const definitionCriteria = ["exit", "clean", "changed", "verdict", "attachment", "status"];
73
+ /* the groups of the options of a definition (their options named "<group>-<item>") */
74
+ export const definitionGroups = ["state", "tag", "assign", "request", "prompt", "prepare", "success", "verdict", "attach", "finish"];
75
+ const opt = (key, kind, arg, desc) => ({ key, kind, arg, desc });
76
+ export const definitionOptions = [
77
+ opt("definition", "value", "file", "YAML, TypeScript, or JavaScript file with the definition of the agent " +
78
+ "(with the same keys as these options, flat or nested by group, plus hooks for TypeScript and JavaScript, " +
79
+ "and optionally a base definition it extends as \"definition\", where the given options win), " +
80
+ "or \"builtin:<kind>[-<mode>]\" for the definition of a built-in agent"),
81
+ opt("title", "value", "text", "one-line title of the operation of the agent " +
82
+ "(shown on the task boards, at most 80 characters)"),
83
+ opt("description", "value", "text", "brief description of the operation of the agent " +
84
+ "(shown on the task boards, about 3-6 lines, at most 600 characters)"),
85
+ opt("type", "value", "type", "processing type: \"phase\" (claim tasks by their lifecycle status) or " +
86
+ "\"request\" (process the attached prompt requests of tasks) (default: \"phase\")"),
87
+ opt("workdir", "value", "kind", "working directory: \"project\", \"source\", \"changeset\", " +
88
+ "\"changeset-read\", or \"target\" (default: \"project\")"),
89
+ opt("lifecycle", "list", "model", "supported task lifecycle model (repeatable) " +
90
+ "(default: every model containing all given states, resp. all models with \"--unclaimed\")"),
91
+ opt("serial", "flag", "", "process the tasks serially only (ignoring \"--parallel\")"),
92
+ opt("unclaimed", "flag", "", "process the tasks of the incoming states (each adapted onto the " +
93
+ "task lifecycle model) without claiming them, independent of their assignee and predecessors, " +
94
+ "and without ever changing their status and assignee (for \"phase\" only)"),
95
+ opt("params", "list", "name=value", "value of a parameter declared by the definition (repeatable)"),
96
+ opt("state-incoming", "list", "state", "state of the tasks to claim (mandatory for \"phase\", " +
97
+ "repeatable with \"--unclaimed\" or \"--state-activity\" only)"),
98
+ opt("state-activity", "value", "state", "state of a task during its processing (default: incoming)"),
99
+ opt("state-success", "value", "state", "target state on success, or \"previous\" for the incoming state " +
100
+ "of the claim (default: incoming)"),
101
+ opt("state-failure", "value", "state", "target state on failure, or \"previous\" for the incoming state " +
102
+ "of the claim (default: incoming)"),
103
+ opt("state-phase", "value", "state", "phase success state, into which the skill itself can move a task " +
104
+ "(default: success, resp. activity for a \"previous\" success)"),
105
+ opt("state-passed", "value", "state", "first state from which on a task has passed the phase " +
106
+ "for its \"After:\" successors (default: phase)"),
107
+ opt("state-verdict", "list", "verdict=state", "target state per verdict (repeatable)"),
108
+ opt("tag-name", "value", "tag", "tag of the processed tasks (default: \"ase-agent-<name>\")"),
109
+ opt("tag-success", "value", "tag", "tag on success: \"<tag-name>\", \"<tag-name>:<value>\", " +
110
+ "or \"none\" (default: \"<tag-name>\")"),
111
+ opt("tag-failure", "value", "tag", "tag on failure: \"<tag-name>\", \"<tag-name>:<value>\", " +
112
+ "or \"none\" (default: \"<tag-name>\")"),
113
+ opt("tag-require", "list", "tag", "tag a claimable task has to carry (repeatable)"),
114
+ opt("tag-exclude", "list", "tag", "tag a claimable task must not carry (repeatable)"),
115
+ opt("tag-retry", "flag", "", "untag a failed task, so it is claimed again (shorthand for \"--tag-failure none\")"),
116
+ opt("assign-name", "value", "who", "assignee under which the tasks are claimed and which names " +
117
+ "the agent-owned worktrees (default: the agent name)"),
118
+ opt("assign-required", "flag", "", "claim only the tasks explicitly assigned to \"--assign-name\""),
119
+ opt("assign-success", "value", "who", "assignee of a task after its successful processing: " +
120
+ "a name, \"none\", or \"previous\" (default: \"none\" for \"phase\", \"previous\" for \"request\")"),
121
+ opt("assign-failure", "value", "who", "assignee of a task after its failed processing: " +
122
+ "a name, \"none\", or \"previous\" (default: \"none\" for \"phase\", \"previous\" for \"request\")"),
123
+ opt("request-name", "value", "name", "name of the processed requests (default: the agent name)"),
124
+ opt("request-done", "value", "word", "word naming a processed task in the log lines (default: \"processed\")"),
125
+ opt("prompt-skill", "value", "name", "skill to invoke"),
126
+ opt("prompt-args", "value", "template", "arguments of the skill " +
127
+ "(default: \"{id}\" for \"phase\", \"{id}: {prompt}\" for \"request\")"),
128
+ opt("prompt-text", "value", "template", "prompt text to invoke (instead of a skill)"),
129
+ opt("prompt-file", "value", "file", "file with the prompt text to invoke (instead of a skill)"),
130
+ opt("prepare-run", "list", "cmd", "shell command to run before the prompt (repeatable)"),
131
+ opt("success-criterion", "list", "criterion", "success criterion: \"exit\", \"clean\", \"changed\", " +
132
+ "\"verdict\", \"attachment\", or \"status\" (repeatable) " +
133
+ "(default: \"exit\" and \"clean\" for \"phase\", \"clean\" and \"changed\" for \"request\")"),
134
+ opt("success-match", "list", "regex", "regular expression an output line has to match (repeatable)"),
135
+ opt("verdict-label", "value", "label", "label after which the verdict is given in the output"),
136
+ opt("verdict-allow", "list", "verdict", "accepted verdict (repeatable) " +
137
+ "(default: the verdicts of \"--state-verdict\", else any)"),
138
+ opt("attach-output", "value", "kind", "kind of the attachment storing the output lines of the prompt"),
139
+ opt("attach-expect", "value", "spec", "attachment \"[<mime-type>;]<kind>\" to be created or changed " +
140
+ "by the processing for the \"attachment\" success criterion"),
141
+ opt("attach-skip", "value", "spec", "attachment \"[<mime-type>;]<kind>\" leaving a task unclaimed"),
142
+ opt("attach-changeset", "value", "kind", "kind of the attachment \"text/x-diff;<kind>\" storing the change set " +
143
+ "snapshot of the task (with \"--workdir changeset\" or \"target\" only)"),
144
+ opt("finish-commit", "flag", "", "commit the work onto a \"branch:\" changeset and publish it " +
145
+ "(with \"--workdir changeset\" only)"),
146
+ opt("finish-success", "value", "cmd", "shell command to run after a successful processing"),
147
+ opt("finish-failure", "value", "cmd", "shell command to run after a failed processing")
148
+ ];
149
+ /* the camel-cased attribute name of an option (as by "commander") */
150
+ export const optionAttr = (key) => key.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
151
+ /* the placeholders of the templates (besides "key:<Header>" and "param:<name>") */
152
+ const placeholders = [
153
+ "id", "title", "prompt", "source", "changeset", "target",
154
+ "source-type", "source-name", "changeset-type", "changeset-name", "target-type", "target-name",
155
+ "workdir", "source-dir", "changeset-worktree"
156
+ ];
157
+ /* the name of a parameter, and the name of the environment variable carrying its value */
158
+ const paramNameRe = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
159
+ export const paramEnv = (name) => `ASE_AGENT_PARAM_${name.toUpperCase().replace(/-/g, "_")}`;
160
+ /* parse a template of the option <option>: placeholders "{<name>}" (where "{prompt}" is
161
+ known for "request" only, and "{param:<name>}" for the declared parameters <params> only),
162
+ conditionals "{?<name>:<text>}" and "{?<name>=<value>:<text>}" (with placeholders, but
163
+ without nested conditionals in <text>), and "{{" and "}}" for literal braces, keeping
164
+ all other text verbatim */
165
+ export const parseTemplate = (spec, option, type, params) => {
166
+ const fail = (msg) => {
167
+ throw new Error(`agent: option "--${option}": ${msg}`);
168
+ };
169
+ const names = new Set();
170
+ const required = new Set();
171
+ const known = (name) => {
172
+ if (name.startsWith("param:")) {
173
+ if (!params.includes(name.slice(6)))
174
+ fail(`unknown placeholder "${name}" (no declared parameter "${name.slice(6)}")`);
175
+ }
176
+ else if (!/^key:[A-Za-z][A-Za-z0-9-]*$/.test(name) && !(placeholders.includes(name) && (name !== "prompt" || type === "request")))
177
+ fail(`unknown placeholder "${name}"` + (name === "prompt" ? " (known for \"--type request\" only)" : ""));
178
+ names.add(name);
179
+ return name;
180
+ };
181
+ /* parse the nodes of the template or, from the position <open> of its opening
182
+ brace on, of the body of a conditional on the non-empty placeholder <guard>
183
+ (else empty), up to its closing brace */
184
+ let i = 0;
185
+ const parse = (open, guard = "") => {
186
+ const nodes = [];
187
+ let text = "";
188
+ const flush = () => {
189
+ if (text !== "")
190
+ nodes.push({ text });
191
+ text = "";
192
+ };
193
+ while (i < spec.length) {
194
+ if (spec.startsWith("{{", i) || spec.startsWith("}}", i)) {
195
+ text += spec[i];
196
+ i += 2;
197
+ }
198
+ else if (spec[i] === "}") {
199
+ if (open < 0)
200
+ fail(`unbalanced "}" at position ${i + 1} (use "}}" for a literal brace)`);
201
+ i++;
202
+ flush();
203
+ return nodes;
204
+ }
205
+ else if (spec.startsWith("{?", i)) {
206
+ if (open >= 0)
207
+ fail(`nested conditional at position ${i + 1}`);
208
+ const m = /^\{\?(key:[A-Za-z][A-Za-z0-9-]*|param:[A-Za-z][A-Za-z0-9_-]*|[a-z][a-z-]*)(?:=([^:}]*))?:/.exec(spec.slice(i));
209
+ if (m === null)
210
+ fail(`invalid conditional at position ${i + 1} (expected "{?<name>:<text>}" or "{?<name>=<value>:<text>}")`);
211
+ const cond = known(m[1]);
212
+ const at = i;
213
+ i += m[0].length;
214
+ flush();
215
+ nodes.push({ cond, equals: m[2] ?? null, body: parse(at, m[2] === undefined ? cond : "") });
216
+ }
217
+ else if (spec[i] === "{") {
218
+ const m = /^\{(key:[A-Za-z][A-Za-z0-9-]*|param:[A-Za-z][A-Za-z0-9_-]*|[a-z][a-z-]*)\}/.exec(spec.slice(i));
219
+ if (m === null)
220
+ fail(`invalid placeholder at position ${i + 1} (use "{{" for a literal brace)`);
221
+ flush();
222
+ nodes.push({ name: known(m[1]) });
223
+ if (m[1] !== guard)
224
+ required.add(m[1]);
225
+ i += m[0].length;
226
+ }
227
+ else
228
+ text += spec[i++];
229
+ }
230
+ if (open >= 0)
231
+ fail(`unclosed conditional at position ${open + 1}`);
232
+ flush();
233
+ return nodes;
234
+ };
235
+ return { nodes: parse(-1), names, required };
236
+ };
237
+ /* render a parsed template with the values of its placeholders */
238
+ export const renderTemplate = (template, value) => {
239
+ const render = (nodes) => nodes.map((node) => {
240
+ if ("text" in node)
241
+ return node.text;
242
+ else if ("name" in node)
243
+ return value(node.name);
244
+ const val = value(node.cond);
245
+ return (node.equals === null ? val !== "" : val === node.equals) ? render(node.body) : "";
246
+ }).join("");
247
+ return render(template.nodes);
248
+ };
249
+ export const attachMatches = (spec, type) => typeParam(type, "kind") === spec.kind
250
+ && (spec.mime === "" || (type.split(";")[0] ?? "").trim().toLowerCase() === spec.mime);
251
+ /* flatten the nested option groups of a definition (e.g. "state: { incoming: PLANNED }"
252
+ into "state-incoming: PLANNED"), where the map "params" is kept as-is, and where
253
+ <where> names the definition in errors */
254
+ export const flattenDefinition = (data, where) => {
255
+ const flat = {};
256
+ const set = (key, value) => {
257
+ if (Object.hasOwn(flat, key))
258
+ throw new Error(`${where}: option "${key}" given twice (flat and nested)`);
259
+ flat[key] = value;
260
+ };
261
+ for (const [key, value] of Object.entries(data)) {
262
+ if (key !== "params" && value !== null && typeof value === "object" && !Array.isArray(value)) {
263
+ if (!definitionGroups.includes(key))
264
+ throw new Error(`${where}: option "${key}" is no option group (expected one of ` +
265
+ `${definitionGroups.map((group) => `"${group}"`).join(", ")})`);
266
+ for (const [item, val] of Object.entries(value))
267
+ set(`${key}-${item}`, val);
268
+ }
269
+ else
270
+ set(key, value);
271
+ }
272
+ return flat;
273
+ };
274
+ /* the names of the hooks of a definition (see "AgentHooks") */
275
+ const hookNames = ["start", "order", "passes", "ready", "prepare", "prompt", "verdict", "success", "settle", "finish", "stop"];
276
+ /* normalize the parameter declarations <spec> of a definition (see "AgentParam"),
277
+ where <where> names the definition in errors */
278
+ const definitionParams = (spec, where) => {
279
+ const params = {};
280
+ if (spec === undefined || spec === null)
281
+ return params;
282
+ if (typeof spec !== "object" || Array.isArray(spec))
283
+ throw new Error(`${where}: invalid parameters (expected an object of parameter declarations)`);
284
+ const scalar = (val) => typeof val === "string" || typeof val === "number" || typeof val === "boolean";
285
+ for (const [name, decl] of Object.entries(spec)) {
286
+ const fail = (msg) => {
287
+ throw new Error(`${where}: parameter "${name}": ${msg}`);
288
+ };
289
+ if (!paramNameRe.test(name))
290
+ fail("invalid name (expected up to 64 letters, digits, underscores, and dashes, starting with a letter)");
291
+ if (scalar(decl)) {
292
+ params[name] = { default: String(decl) };
293
+ continue;
294
+ }
295
+ if (decl === null || typeof decl !== "object" || Array.isArray(decl))
296
+ fail("invalid declaration (expected its default value or an object of \"desc\", \"default\", \"required\", and \"allow\")");
297
+ const param = {};
298
+ for (const [key, val] of Object.entries(decl)) {
299
+ if (val === undefined || val === null)
300
+ continue;
301
+ else if (key === "desc" && typeof val === "string")
302
+ param.desc = val;
303
+ else if (key === "default" && scalar(val))
304
+ param.default = String(val);
305
+ else if (key === "required" && typeof val === "boolean")
306
+ param.required = val;
307
+ else if (key === "allow" && (Array.isArray(val) ? val : [val]).every((item) => scalar(item)))
308
+ param.allow = (Array.isArray(val) ? val : [val]).map((item) => String(item));
309
+ else if (["desc", "default", "required", "allow"].includes(key))
310
+ fail(`invalid value of "${key}"`);
311
+ else
312
+ fail(`unknown field "${key}" (expected "desc", "default", "required", or "allow")`);
313
+ }
314
+ params[name] = param;
315
+ }
316
+ return params;
317
+ };
318
+ /* normalize the (parsed or imported) data of a definition into its flattened option values,
319
+ its parameter declarations, its hooks, and its base definition, where <where> names the
320
+ definition in errors, and a relative "prompt-file" and a relative base definition file
321
+ are resolved against the directory <dir> (if given) */
322
+ const definitionValues = (data, where, dir) => {
323
+ if (data === null || typeof data !== "object" || Array.isArray(data))
324
+ throw new Error(`${where} carries no options`);
325
+ const { hooks: spec, definition: base, params: decls, ...options } = data;
326
+ if (base !== undefined && base !== null && (typeof base !== "string" || base === ""))
327
+ throw new Error(`${where}: invalid base definition (expected a file or "builtin:<kind>[-<mode>]")`);
328
+ const params = definitionParams(decls, where);
329
+ const hooks = {};
330
+ if (spec !== undefined && spec !== null) {
331
+ if (typeof spec !== "object" || Array.isArray(spec))
332
+ throw new Error(`${where}: invalid hooks (expected an object of functions)`);
333
+ for (const [name, fn] of Object.entries(spec)) {
334
+ if (!hookNames.includes(name))
335
+ throw new Error(`${where}: unknown hook "${name}" ` +
336
+ `(expected ${hookNames.map((hook) => `"${hook}"`).join(", ")})`);
337
+ if (typeof fn !== "function")
338
+ throw new Error(`${where}: hook "${name}" is no function ` +
339
+ "(hooks require a TypeScript or JavaScript definition file)");
340
+ }
341
+ Object.assign(hooks, spec);
342
+ }
343
+ const values = {};
344
+ for (const [key, value] of Object.entries(flattenDefinition(options, where))) {
345
+ const option = definitionOptions.find((opt) => opt.key === key);
346
+ if (option === undefined)
347
+ throw new Error(`${where}: unknown option "${key}"`);
348
+ const scalar = (val) => {
349
+ if (typeof val !== "string" && typeof val !== "number")
350
+ throw new Error(`${where}: invalid value of option "${key}" (expected a string)`);
351
+ return String(val);
352
+ };
353
+ if (value === null || value === undefined)
354
+ continue;
355
+ else if (option.kind === "flag") {
356
+ if (typeof value !== "boolean")
357
+ throw new Error(`${where}: invalid value of option "${key}" (expected a boolean)`);
358
+ values[key] = value;
359
+ }
360
+ else if (option.kind === "list")
361
+ values[key] = (Array.isArray(value) ? value : [value]).map(scalar);
362
+ else
363
+ values[key] = scalar(value);
364
+ }
365
+ if (typeof values["prompt-file"] === "string" && dir !== "")
366
+ values["prompt-file"] = path.resolve(dir, values["prompt-file"]);
367
+ const guards = hooks.ready !== undefined && values["tag-success"] === "none";
368
+ if (typeof base !== "string")
369
+ return { values, params, hooks, guards };
370
+ return { values, params, hooks, guards, base: base.startsWith("builtin:") || dir === "" ? base : path.resolve(dir, base) };
371
+ };
372
+ /* the mutually exclusive alternatives of the options, which a given option replaces in the
373
+ definition (instead of being merged with them) */
374
+ const alternatives = {
375
+ "prompt-skill": ["prompt-text", "prompt-file"],
376
+ "prompt-text": ["prompt-skill", "prompt-args", "prompt-file"],
377
+ "prompt-file": ["prompt-skill", "prompt-args", "prompt-text"],
378
+ "tag-retry": ["tag-failure"],
379
+ "tag-failure": ["tag-retry"]
380
+ };
381
+ /* overlay the option values <upper> onto the option values <lower>, where an option of
382
+ <upper> (except a false flag) also replaces its mutually exclusive alternatives of <lower> */
383
+ const overlayValues = (lower, upper) => {
384
+ const masked = new Set(Object.entries(upper)
385
+ .filter(([, value]) => value !== false)
386
+ .flatMap(([key]) => alternatives[key] ?? []));
387
+ return { ...Object.fromEntries(Object.entries(lower).filter(([key]) => !masked.has(key))), ...upper };
388
+ };
389
+ /* load the definition of "--definition": the definition of a built-in agent of the
390
+ specification "builtin:<kind>[-<mode>]" (default: its default mode), else a YAML file
391
+ (".yaml" or ".yml"), or a TypeScript (".ts" or ".mts", via the type stripping of
392
+ Node.js) or JavaScript file (".js", ".mjs", or ".cjs"), whose default export is the
393
+ definition or a (possibly asynchronous) function returning it, where a definition
394
+ with a base definition (its key "definition") extends the latter: its options override
395
+ those of the base definition (also replacing their mutually exclusive alternatives,
396
+ and with a false flag unsetting the flag of the base definition), its parameter
397
+ declarations extend those of the same name of the base definition field-wise, and its
398
+ hooks replace those of the same name of the base definition, while <chain> carries the
399
+ already loading definitions of a chain of base definitions (to detect a cycle) */
400
+ const loadDefinition = async (spec, chain = []) => {
401
+ let loaded;
402
+ let id;
403
+ if (spec.startsWith("builtin:")) {
404
+ const where = `agent: option "--definition": definition "${spec}"`;
405
+ const m = /^builtin:([a-z]+)(?:-([a-z]+))?$/.exec(spec);
406
+ const kind = builtinKinds.find((kind) => kind.kind === m?.[1]);
407
+ const mode = m?.[2] ?? kind?.mode ?? "";
408
+ if (m === null || kind === undefined || !Object.hasOwn(kind.modes, mode))
409
+ throw new Error(`${where}: unknown built-in definition (expected ` +
410
+ builtinKinds.flatMap((kind) => Object.keys(kind.modes).map((mode) => `"builtin:${kind.kind}-${mode}"`)).join(", ") + ")");
411
+ id = `builtin:${kind.kind}-${mode}`;
412
+ loaded = definitionValues(kind.modes[mode], where, "");
413
+ }
414
+ else {
415
+ id = path.resolve(spec);
416
+ loaded = await loadDefinitionFile(id);
417
+ }
418
+ if (loaded.base !== undefined) {
419
+ if (chain.includes(id))
420
+ throw new Error(`agent: option "--definition": definition "${id}": cyclic base definitions ` +
421
+ `(${[...chain.slice(chain.indexOf(id)), id].map((def) => `"${def}"`).join(" > ")})`);
422
+ const base = await loadDefinition(loaded.base, [...chain, id]);
423
+ const params = { ...base.params };
424
+ for (const [name, param] of Object.entries(loaded.params))
425
+ params[name] = { ...params[name], ...param };
426
+ loaded = {
427
+ values: overlayValues(base.values, loaded.values),
428
+ params,
429
+ hooks: { ...base.hooks, ...loaded.hooks },
430
+ guards: loaded.guards || (base.guards && loaded.hooks.ready === undefined)
431
+ };
432
+ }
433
+ if (chain.length === 0)
434
+ loaded.values = Object.fromEntries(Object.entries(loaded.values).filter(([, value]) => value !== false));
435
+ return loaded;
436
+ };
437
+ /* load the definition file <file>: a YAML, TypeScript, or JavaScript file (see "loadDefinition") */
438
+ const loadDefinitionFile = async (file) => {
439
+ let data;
440
+ try {
441
+ if (/\.ya?ml$/i.test(file))
442
+ data = YAML.parse(fs.readFileSync(file, "utf8"));
443
+ else if (/\.(?:[cm]?js|m?ts)$/i.test(file)) {
444
+ if (/ts$/i.test(file) && !process.features.typescript)
445
+ throw new Error("TypeScript requires the type stripping of Node.js (Node.js 22.18.0 or newer)");
446
+ const mod = await import(pathToFileURL(file).href);
447
+ data = typeof mod.default === "function" ? await mod.default() : mod.default;
448
+ if (data === undefined)
449
+ throw new Error("no default export");
450
+ }
451
+ else
452
+ throw new Error("unknown file type (expected \".yaml\", \".yml\", \".ts\", \".mts\", " +
453
+ "\".js\", \".mjs\", or \".cjs\")");
454
+ }
455
+ catch (err) {
456
+ throw new Error(`agent: option "--definition": cannot read definition "${file}": ` +
457
+ `${err instanceof Error ? err.message : String(err)}`, { cause: err });
458
+ }
459
+ return definitionValues(data, `agent: option "--definition": definition "${file}"`, path.dirname(file));
460
+ };
461
+ /* whether a phase agent claiming only the tasks explicitly assigned to it (<required>, under
462
+ the assignee <name>) hands a processed task over to the assignee specification <who> of
463
+ somebody else, so an untagged task returning into an incoming state is guarded against
464
+ being claimed again by its assignment */
465
+ export const unassignedFrom = (required, name, who) => required && who !== "previous" && who !== name;
466
+ /* fail on the option "--<key>" with the message <msg> */
467
+ const failOption = (key, msg) => {
468
+ throw new Error(`agent: option "--${key}": ${msg}`);
469
+ };
470
+ /* resolve the values of the declared parameters <decls>: their given values <specs>
471
+ ("<name>=<value>", where a later value of the same name wins), else their default
472
+ values, else empty, validated against their declarations */
473
+ const resolveParams = (decls, specs) => {
474
+ const params = {};
475
+ const envs = new Map();
476
+ for (const [name, decl] of Object.entries(decls)) {
477
+ const other = envs.get(paramEnv(name));
478
+ if (other !== undefined)
479
+ failOption("definition", `parameters "${other}" and "${name}" clash in their environment variable "${paramEnv(name)}"`);
480
+ envs.set(paramEnv(name), name);
481
+ params[name] = decl.default ?? "";
482
+ }
483
+ const declared = Object.keys(decls).map((name) => `"${name}"`).join(", ");
484
+ for (const spec of specs) {
485
+ const m = /^([^=]+)=(.*)$/s.exec(spec);
486
+ if (m === null)
487
+ failOption("params", `invalid value "${spec}" (expected "<name>=<value>")`);
488
+ else if (!Object.hasOwn(decls, m[1]))
489
+ failOption("params", `unknown parameter "${m[1]}" ` +
490
+ (declared !== "" ? `(expected ${declared})` : "(the definition declares no parameters)"));
491
+ params[m[1]] = m[2];
492
+ }
493
+ for (const [name, decl] of Object.entries(decls)) {
494
+ const value = params[name];
495
+ if (decl.required === true && value === "")
496
+ failOption("params", `parameter "${name}" is required` + (decl.desc !== undefined ? ` (${decl.desc})` : ""));
497
+ else if (decl.allow !== undefined && value !== "" && !decl.allow.includes(value))
498
+ failOption("params", `invalid value "${value}" of parameter "${name}" ` +
499
+ `(expected ${decl.allow.map((val) => `"${val}"`).join(", ")})`);
500
+ }
501
+ return params;
502
+ };
503
+ /* resolve the states of the processing type <type> through the option accessors <str>,
504
+ <list>, and <has>, validated against the task lifecycle models, where the incoming
505
+ states of an <unclaimed> agent are adapted onto the task lifecycle model of the project
506
+ instead (so it supports every model, e.g. with "PLANNED" as "PLANNING" for "team" and
507
+ as "OPEN" for "solo"), as it never changes the status of a task */
508
+ const resolveStates = (str, list, has, type, unclaimed) => {
509
+ const fail = failOption;
510
+ const verdicts = {};
511
+ for (const spec of list("state-verdict")) {
512
+ const m = /^([A-Z]+)=([A-Z]+)$/.exec(spec);
513
+ if (m === null)
514
+ fail("state-verdict", `invalid value "${spec}" (expected "<VERDICT>=<STATE>")`);
515
+ verdicts[m[1]] = m[2];
516
+ }
517
+ const upper = (key, def) => str(key, def).toUpperCase();
518
+ const incoming = [...new Set(list("state-incoming").map((state) => state.toUpperCase()).filter((state) => state !== ""))];
519
+ if (type === "phase" && incoming.length === 0)
520
+ fail("state-incoming", "mandatory for \"--type phase\"");
521
+ else if (incoming.length > 1 && !unclaimed && !has("state-activity"))
522
+ fail("state-incoming", "multiple states allowed with \"--unclaimed\" or \"--state-activity\" only");
523
+ const first = incoming[0] ?? "";
524
+ const activity = upper("state-activity", first);
525
+ const success = upper("state-success", first);
526
+ const failure = upper("state-failure", first);
527
+ const phase = upper("state-phase", success === statePrevious ? activity : success);
528
+ const passed = upper("state-passed", phase);
529
+ /* the states of the claim, where a "previous" success or failure state stands
530
+ for all incoming states (as each one can be the incoming state of the claim) */
531
+ const targets = (state) => state === statePrevious ? incoming : [state];
532
+ const claimKeys = unclaimed ? [] : [
533
+ ["state-activity", activity],
534
+ ...targets(success).map((state) => ["state-success", state]),
535
+ ...targets(failure).map((state) => ["state-failure", state]),
536
+ ["state-phase", phase], ["state-passed", passed],
537
+ ...Object.values(verdicts).map((state) => ["state-verdict", state])
538
+ ];
539
+ const stateKeys = type === "phase" ? [
540
+ ...incoming.map((state) => ["state-incoming", state]), ...claimKeys
541
+ ] : [];
542
+ const models = Object.values(TaskFormat.taskLifecycles);
543
+ for (const model of list("lifecycle"))
544
+ if (!Object.hasOwn(TaskFormat.taskLifecycles, model))
545
+ fail("lifecycle", `unknown task lifecycle model "${model}" ` +
546
+ `(expected ${models.map((m) => `"${m.name}"`).join(", ")})`);
547
+ for (const [key, state] of stateKeys)
548
+ if (!models.some((model) => model.states.includes(state)))
549
+ fail(key, `state "${state}" is not a state of any task lifecycle model`);
550
+ const lifecycles = has("lifecycle") ? [...new Set(list("lifecycle"))] :
551
+ unclaimed ? models.map((model) => model.name) :
552
+ models.filter((model) => stateKeys.every(([, state]) => model.states.includes(state))).map((model) => model.name);
553
+ if (lifecycles.length === 0)
554
+ fail("lifecycle", "no task lifecycle model contains all the given states");
555
+ for (const model of lifecycles.map((name) => TaskFormat.taskLifecycles[name])) {
556
+ if (unclaimed)
557
+ continue;
558
+ for (const [key, state] of stateKeys)
559
+ if (!model.states.includes(state))
560
+ fail(key, `state "${state}" is not a state of the "${model.name}" task lifecycle model`);
561
+ if (type !== "phase")
562
+ continue;
563
+ const transitions = [
564
+ ...incoming.flatMap((state) => [
565
+ ["state-activity", state, activity], ["state-activity", activity, state]
566
+ ]),
567
+ ...targets(success).map((state) => ["state-success", activity, state]),
568
+ ...targets(failure).map((state) => ["state-failure", activity, state]),
569
+ ...Object.values(verdicts).map((state) => ["state-verdict", activity, state])
570
+ ];
571
+ for (const [key, from, to] of transitions) {
572
+ const error = TaskFormat.checkStatus(model, from, to);
573
+ if (error !== "")
574
+ fail(key, error);
575
+ }
576
+ }
577
+ return { incoming, activity, success, failure, phase, passed, verdicts, lifecycles };
578
+ };
579
+ /* resolve the definition of the agent named <name> from its (camel-cased) command-line
580
+ options <opts>: the defaults, overridden by the definition of "--definition" (if any),
581
+ overridden by the given options, validated against the task lifecycle models */
582
+ export const resolveDefinition = async (opts, name) => {
583
+ /* merge the values of the definition and of the given options, where a given
584
+ option also replaces its mutually exclusive alternatives of the definition */
585
+ const given = {};
586
+ for (const option of definitionOptions) {
587
+ const value = opts[optionAttr(option.key)];
588
+ if (value !== undefined && value !== false)
589
+ given[option.key] = value;
590
+ }
591
+ const loaded = typeof given.definition === "string" ? await loadDefinition(given.definition) : { values: {}, params: {}, hooks: {}, guards: false };
592
+ const values = overlayValues(loaded.values, given);
593
+ const str = (key, def = "") => typeof values[key] === "string" ? values[key] : def;
594
+ const list = (key) => Array.isArray(values[key]) ? values[key] : [];
595
+ const flag = (key) => values[key] === true;
596
+ const has = (key) => values[key] !== undefined;
597
+ const fail = failOption;
598
+ const choice = (key, choices, def) => {
599
+ const value = str(key, def);
600
+ if (!choices.includes(value))
601
+ fail(key, `invalid value "${value}" (expected ${choices.map((c) => `"${c}"`).join(", ")})`);
602
+ return value;
603
+ };
604
+ /* the title and the description of the operation, each with its whitespace collapsed
605
+ (as the task boards wrap the description onto their width themselves) */
606
+ const prose = (key, max) => {
607
+ const text = str(key).replace(/\s+/g, " ").trim();
608
+ if (text.length > max)
609
+ fail(key, `too long text (${text.length} characters, expected at most ${max})`);
610
+ return text;
611
+ };
612
+ const title = prose("title", 80);
613
+ const description = prose("description", 600);
614
+ /* the processing type, the working directory, and the type-specific options */
615
+ const type = choice("type", definitionTypes, "phase");
616
+ const workdir = choice("workdir", definitionWorkdirs, "project");
617
+ const phaseOnly = ["unclaimed", "state-incoming", "state-activity", "state-success", "state-failure", "state-phase",
618
+ "state-passed", "state-verdict", "tag-success", "tag-failure", "tag-retry", "assign-required", "attach-skip"];
619
+ const requestOnly = ["request-name", "request-done"];
620
+ for (const key of type === "request" ? phaseOnly : requestOnly)
621
+ if (has(key))
622
+ fail(key, `not allowed with "--type ${type}" (for "--type ${type === "request" ? "phase" : "request"}" only)`);
623
+ if (has("finish-commit") && workdir !== "changeset")
624
+ fail("finish-commit", `not allowed with "--workdir ${workdir}" (for "--workdir changeset" only)`);
625
+ if (has("attach-changeset") && workdir !== "changeset" && workdir !== "target")
626
+ fail("attach-changeset", `not allowed with "--workdir ${workdir}" (for "--workdir changeset" or "target" only)`);
627
+ if (loaded.hooks.passes !== undefined && type !== "phase")
628
+ fail("definition", "hook \"passes\" is for \"--type phase\" only");
629
+ /* the unclaimed processing of a phase agent, which never changes the status or the
630
+ assignee of a task, never waits for its predecessors, and never writes into the
631
+ changeset or the target of a task (as another agent can claim the task meanwhile) */
632
+ const unclaimed = flag("unclaimed");
633
+ if (unclaimed) {
634
+ const claimedOnly = ["state-activity", "state-success", "state-failure", "state-phase", "state-passed",
635
+ "state-verdict", "assign-required", "assign-success", "assign-failure"];
636
+ for (const key of claimedOnly)
637
+ if (has(key))
638
+ fail(key, "not allowed with \"--unclaimed\" (as the status and the assignee of an unclaimed task are never changed)");
639
+ if (workdir === "changeset" || workdir === "target")
640
+ fail("workdir", `"${workdir}" not allowed with "--unclaimed" (as an unclaimed task can be claimed by another agent meanwhile)`);
641
+ if (loaded.hooks.passes !== undefined)
642
+ fail("definition", "hook \"passes\" is not for \"--unclaimed\" (as an unclaimed task never waits for its predecessors)");
643
+ }
644
+ /* whether a verdict is determined (by "--verdict-label" or the hook "verdict") */
645
+ const verdicting = has("verdict-label") || loaded.hooks.verdict !== undefined;
646
+ /* the states (see "resolveStates") */
647
+ const { incoming, activity, success, failure, phase, passed, verdicts, lifecycles } = resolveStates(str, list, has, type, unclaimed);
648
+ /* the tag and the assignee */
649
+ const assignName = str("assign-name", name);
650
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]*$/.test(assignName))
651
+ fail("assign-name", `invalid assignee "${assignName}"`);
652
+ const requestName = str("request-name", name);
653
+ let tagName = str("tag-name", "");
654
+ if (type === "request") {
655
+ let tag = "";
656
+ try {
657
+ tag = requestsOf(requestName).tag;
658
+ }
659
+ catch (err) {
660
+ fail(has("request-name") ? "request-name" : "name", err instanceof Error ? err.message : String(err));
661
+ }
662
+ if (tagName !== "" && tagName !== tag)
663
+ fail("tag-name", `has to be "${tag}" for the requests "${requestName}" (as attached by "ase task request")`);
664
+ tagName = tag;
665
+ }
666
+ else if (tagName === "")
667
+ tagName = `ase-agent-${name.toLowerCase()}`;
668
+ if (!/^ase-agent-[a-z0-9][a-z0-9_.-]*$/.test(tagName))
669
+ fail("tag-name", `invalid tag "${tagName}" (expected "ase-agent-<word>" in lower-case, without ":")`);
670
+ const tagValue = (key) => {
671
+ const spec = str(key, tagName);
672
+ if (spec === "none")
673
+ return null;
674
+ else if (spec === tagName)
675
+ return undefined;
676
+ const m = /^([^:]+):([a-z0-9][a-z0-9_.-]*)$/.exec(spec);
677
+ if (m === null || m[1] !== tagName)
678
+ fail(key, `invalid tag "${spec}" (expected "${tagName}", "${tagName}:<value>" in lower-case, or "none", ` +
679
+ "as a foreign tag would let the agent claim the task again)");
680
+ if (m[2] === "active")
681
+ fail(key, `invalid tag "${spec}" (the value "active" marks a claimed task)`);
682
+ return m[2];
683
+ };
684
+ if (flag("tag-retry") && has("tag-failure"))
685
+ fail("tag-retry", "not allowed together with \"--tag-failure\"");
686
+ const assignee = (key, def) => {
687
+ const who = str(key, def);
688
+ if (who !== "none" && who !== "previous" && !/^[A-Za-z0-9][A-Za-z0-9_.@-]*$/.test(who))
689
+ fail(key, `invalid assignee "${who}" (expected a name, "none", or "previous")`);
690
+ return who;
691
+ };
692
+ const handover = type === "phase" ? "none" : "previous";
693
+ const assignSuccess = assignee("assign-success", handover);
694
+ const assignFailure = assignee("assign-failure", handover);
695
+ const tagSuccess = tagValue("tag-success");
696
+ const tagFailure = flag("tag-retry") ? null : tagValue("tag-failure");
697
+ const returning = (state) => state === statePrevious || incoming.includes(state);
698
+ if (tagSuccess === null && type === "phase" && (returning(success) || Object.values(verdicts).some(returning))
699
+ && !has("attach-skip") && !loaded.guards && !unassignedFrom(flag("assign-required"), assignName, assignSuccess))
700
+ fail("tag-success", "\"none\" would let the agent claim a task again, which returns into an incoming state " +
701
+ "(unless guarded by \"--attach-skip\", by the hook \"ready\" of a definition declaring \"tag-success: none\" itself, " +
702
+ "or by \"--assign-required\" with \"--assign-success\" handing the task over to somebody else)");
703
+ for (const key of ["tag-require", "tag-exclude"])
704
+ for (const tag of list(key))
705
+ if (!/^[^\s,]+$/.test(tag))
706
+ fail(key, `invalid tag "${tag}"`);
707
+ /* the parameters (see "resolveParams") */
708
+ const decls = loaded.params;
709
+ const params = resolveParams(decls, list("params"));
710
+ /* the prompt */
711
+ const prompts = ["prompt-skill", "prompt-text", "prompt-file"].filter((key) => has(key));
712
+ if (prompts.length !== 1)
713
+ fail(prompts[1] ?? "prompt-skill", "exactly one of \"--prompt-skill\", \"--prompt-text\", or \"--prompt-file\" is required");
714
+ if (has("prompt-args") && !has("prompt-skill"))
715
+ fail("prompt-args", "allowed with \"--prompt-skill\" only");
716
+ const skill = str("prompt-skill");
717
+ if (has("prompt-skill") && !/^(?:[A-Za-z0-9_.-]+:)?[A-Za-z0-9_.-]+$/.test(skill))
718
+ fail("prompt-skill", `invalid skill name "${skill}"`);
719
+ let template;
720
+ if (has("prompt-skill"))
721
+ template = parseTemplate(str("prompt-args", type === "phase" ? "{id}" : "{id}: {prompt}"), "prompt-args", type, Object.keys(decls));
722
+ else if (has("prompt-text"))
723
+ template = parseTemplate(str("prompt-text"), "prompt-text", type, Object.keys(decls));
724
+ else {
725
+ let text = "";
726
+ try {
727
+ text = fs.readFileSync(path.resolve(str("prompt-file")), "utf8");
728
+ }
729
+ catch (err) {
730
+ fail("prompt-file", `cannot read prompt file: ${err instanceof Error ? err.message : String(err)}`);
731
+ }
732
+ template = parseTemplate(text, "prompt-file", type, Object.keys(decls));
733
+ }
734
+ /* the success criteria and the verdict */
735
+ const criteria = has("success-criterion") ? [...new Set(list("success-criterion"))] :
736
+ (type === "phase" ? ["exit", "clean"] : ["clean", "changed"]);
737
+ for (const criterion of criteria) {
738
+ if (!definitionCriteria.includes(criterion))
739
+ fail("success-criterion", `invalid criterion "${criterion}" ` +
740
+ `(expected ${definitionCriteria.map((c) => `"${c}"`).join(", ")})`);
741
+ else if (criterion === "changed" && type !== "request")
742
+ fail("success-criterion", "criterion \"changed\" is for \"--type request\" only");
743
+ else if (criterion === "status" && type !== "phase")
744
+ fail("success-criterion", "criterion \"status\" is for \"--type phase\" only");
745
+ else if (criterion === "status" && unclaimed)
746
+ fail("success-criterion", "criterion \"status\" is not for \"--unclaimed\" (as an unclaimed task has no phase success state)");
747
+ else if (criterion === "verdict" && !verdicting)
748
+ fail("success-criterion", "criterion \"verdict\" requires \"--verdict-label\" (or the hook \"verdict\")");
749
+ else if (criterion === "attachment" && !has("attach-expect"))
750
+ fail("success-criterion", "criterion \"attachment\" requires \"--attach-expect\"");
751
+ }
752
+ const match = list("success-match").map((spec) => {
753
+ try {
754
+ return new RegExp(spec);
755
+ }
756
+ catch (err) {
757
+ return fail("success-match", `invalid regular expression "${spec}": ${err instanceof Error ? err.message : String(err)}`);
758
+ }
759
+ });
760
+ if ((Object.keys(verdicts).length > 0 || has("verdict-allow")) && !verdicting)
761
+ fail(Object.keys(verdicts).length > 0 ? "state-verdict" : "verdict-allow", "requires \"--verdict-label\" (or the hook \"verdict\")");
762
+ const allow = has("verdict-allow") ? list("verdict-allow") : Object.keys(verdicts);
763
+ for (const verdict of allow)
764
+ if (!/^[A-Z]+$/.test(verdict))
765
+ fail("verdict-allow", `invalid verdict "${verdict}" (expected an upper-case word)`);
766
+ for (const verdict of Object.keys(verdicts))
767
+ if (!allow.includes(verdict))
768
+ fail("state-verdict", `verdict "${verdict}" is not accepted by "--verdict-allow"`);
769
+ /* the attachments */
770
+ const kindRe = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
771
+ const attachSpec = (key) => {
772
+ if (!has(key))
773
+ return null;
774
+ const spec = str(key);
775
+ const i = spec.indexOf(";");
776
+ const mime = i < 0 ? "" : spec.slice(0, i).trim().toLowerCase();
777
+ const kind = i < 0 ? spec.trim() : spec.slice(i + 1).trim();
778
+ if (!kindRe.test(kind) || (i >= 0 && !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)))
779
+ fail(key, `invalid attachment "${spec}" (expected "[<mime-type>;]<kind>")`);
780
+ return { mime, kind };
781
+ };
782
+ if (has("attach-output") && !kindRe.test(str("attach-output")))
783
+ fail("attach-output", `invalid kind "${str("attach-output")}"`);
784
+ else if (str("attach-output") === "agent")
785
+ fail("attach-output", "kind \"agent\" is reserved for the processing log");
786
+ if (has("attach-changeset") && !kindRe.test(str("attach-changeset")))
787
+ fail("attach-changeset", `invalid kind "${str("attach-changeset")}"`);
788
+ const attachExpect = attachSpec("attach-expect");
789
+ if (attachExpect !== null && has("attach-output")
790
+ && attachMatches(attachExpect, `text/plain; kind="${str("attach-output")}"`))
791
+ fail("attach-expect", `attachment "${str("attach-expect")}" is always met by the output attachment of "--attach-output"`);
792
+ for (const key of ["prepare-run", "finish-success", "finish-failure"])
793
+ if ((has(key) && str(key, "-") === "") || list(key).some((cmd) => cmd.trim() === ""))
794
+ fail(key, "empty shell command");
795
+ /* whether a request agent requires the prompts of its requests: by the placeholder
796
+ "{prompt}" of its prompt template, or potentially by a hook receiving the job (and
797
+ hence the prompt of the request), resp. uses them optionally by "{prompt}" used only
798
+ within the conditional "{?prompt:...}", else ignores them */
799
+ const consumes = type === "request" && (template.required.has("prompt")
800
+ || ["prepare", "prompt", "finish"].some((hook) => loaded.hooks[hook] !== undefined)) ? "required" :
801
+ type === "request" && template.names.has("prompt") ? "optional" : "ignored";
802
+ return {
803
+ name, title, description, type, workdir, lifecycles, unclaimed,
804
+ serial: flag("serial"),
805
+ state: { incoming, activity, success, failure, phase, passed, verdict: verdicts },
806
+ tag: {
807
+ name: tagName, success: tagSuccess, failure: tagFailure, require: list("tag-require"), exclude: list("tag-exclude"),
808
+ guarded: loaded.guards
809
+ },
810
+ assign: {
811
+ name: assignName, required: flag("assign-required"),
812
+ success: assignSuccess, failure: assignFailure
813
+ },
814
+ request: { name: requestName, done: str("request-done", "processed"), prompt: consumes },
815
+ prompt: { skill, template },
816
+ prepare: list("prepare-run"),
817
+ success: { criteria: criteria, match },
818
+ verdict: { label: str("verdict-label"), allow },
819
+ attach: {
820
+ output: str("attach-output"), expect: attachExpect, skip: attachSpec("attach-skip"),
821
+ changeset: str("attach-changeset")
822
+ },
823
+ finish: { commit: flag("finish-commit"), success: str("finish-success"), failure: str("finish-failure") },
824
+ params,
825
+ hooks: loaded.hooks
826
+ };
827
+ };