@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,275 @@
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 fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { stripVTControlCharacters } from "node:util";
11
+ import { AsyncLocalStorage } from "node:async_hooks";
12
+ import { execa } from "execa";
13
+ import { Worktree } from "./ase-util-worktree.js";
14
+ /* the hash value of "%h" in "--tempdir", unique to the agent process */
15
+ const tempHash = crypto.randomBytes(4).toString("hex");
16
+ /* the files in a temporary directory of an agent recording the process id of the
17
+ owning agent process, and marking work preserved for a manual recovery */
18
+ const tempOwner = ".owner";
19
+ export const tempPreserved = ".preserved";
20
+ /* whether a process of this host is alive (where one of another user counts as alive) */
21
+ export const pidAlive = (pid) => {
22
+ if (!Number.isInteger(pid) || pid <= 0)
23
+ return false;
24
+ try {
25
+ process.kill(pid, 0);
26
+ return true;
27
+ }
28
+ catch (err) {
29
+ return err.code === "EPERM";
30
+ }
31
+ };
32
+ /* the start time of a process (as rendered by "ps", else empty if unavailable, e.g. on
33
+ Windows), which identifies the process beyond its process id (as the latter is reused) */
34
+ export const pidStart = async (pid) => {
35
+ if (!Number.isInteger(pid) || pid <= 0)
36
+ return "";
37
+ const result = await execa("ps", ["-o", "lstart=", "-p", String(pid)], { reject: false, stdin: "ignore", timeout: 10 * 1000 });
38
+ return result.exitCode === 0 ? result.stdout.trim() : "";
39
+ };
40
+ /* create the private temporary directory of an agent: without a "--tempdir" specification
41
+ <spec> securely as a fresh, unique directory below the system temporary directory, else as
42
+ the sub-directory <name> of the "--tempdir" directory, whose "%p" is expanded to the process
43
+ id, "%h" to the hash value, and "%%" to "%", which is created privately if missing, and else
44
+ has to be a non-aliased directory owned by the current user (a leftover sub-directory of an
45
+ earlier run which stopped before its cleanup is replaced, but neither one of a still running
46
+ agent process, e.g. of another "ase agent" with the same agent name and the same fixed
47
+ "--tempdir", nor one holding work preserved for a manual recovery) */
48
+ export const tempDir = async (name, spec) => {
49
+ if (spec === "")
50
+ return { dir: await fs.promises.realpath(await fs.promises.mkdtemp(path.join(os.tmpdir(), `ase-agent-${name}-`))), base: "" };
51
+ const base = path.resolve(spec.replace(/%([ph%])/g, (_m, c) => c === "p" ? String(process.pid) : c === "h" ? tempHash : "%"));
52
+ const created = await fs.promises.mkdir(base, { recursive: true, mode: 0o700 }) !== undefined;
53
+ const st = await fs.promises.lstat(base);
54
+ if (st.isSymbolicLink() || !st.isDirectory() || (process.getuid !== undefined && st.uid !== process.getuid()))
55
+ throw new Error(`agent: temporary directory "${base}" is no directory owned by the current user`);
56
+ const dir = path.join(await fs.promises.realpath(base), name);
57
+ const inUse = () => new Error(`agent: temporary directory "${dir}" is in use by another running agent process ` +
58
+ "(use a \"--tempdir\" with \"%p\" or \"%h\", or another \"--name\")");
59
+ if (fs.existsSync(dir)) {
60
+ const pid = Number.parseInt(await fs.promises.readFile(path.join(dir, tempOwner), "utf8").catch(() => ""), 10);
61
+ if (pid !== process.pid && pidAlive(pid))
62
+ throw inUse();
63
+ if (fs.existsSync(path.join(dir, tempPreserved)))
64
+ throw new Error(`agent: temporary directory "${dir}" holds work preserved by an earlier run ` +
65
+ `(see "${path.join(dir, tempPreserved)}") -- recover and remove it manually`);
66
+ await fs.promises.rm(dir, { recursive: true, force: true });
67
+ }
68
+ await fs.promises.mkdir(dir, { mode: 0o700 }).catch((err) => {
69
+ throw err.code === "EEXIST" ? inUse() : err;
70
+ });
71
+ await fs.promises.writeFile(path.join(dir, tempOwner), `${process.pid}\n`);
72
+ return { dir, base: created ? base : "" };
73
+ };
74
+ /* the tails of the in-process mutual exclusions per key */
75
+ const exclusions = new Map();
76
+ /* run an operation in mutual exclusion to all other operations of the same key in this process */
77
+ export const exclusive = async (key, op) => {
78
+ const prev = exclusions.get(key) ?? Promise.resolve();
79
+ let done = () => { };
80
+ const gate = new Promise((resolve) => { done = resolve; });
81
+ const tail = prev.then(() => gate);
82
+ exclusions.set(key, tail);
83
+ try {
84
+ await prev;
85
+ return await op();
86
+ }
87
+ finally {
88
+ done();
89
+ if (exclusions.get(key) === tail)
90
+ exclusions.delete(key);
91
+ }
92
+ };
93
+ /* the fixed maximum duration of the preparing and the finishing of a job,
94
+ and of each Git command run by them, so a hanging one (e.g. a "git push"
95
+ waiting for credentials) is killed instead of left behind */
96
+ const jobTimeout = 240 * 1000;
97
+ /* the cancellation of the operation of a job currently run (see "withinJobTimeout"),
98
+ implicitly passed to every command run within it, so a timed out operation
99
+ does not leave its commands running (e.g. into the cleaning up of the job) */
100
+ export const jobScope = new AsyncLocalStorage();
101
+ const execaJobBase = execa({ timeout: jobTimeout, env: { GIT_TERMINAL_PROMPT: "0" } });
102
+ export const execaJob = (file, args, options = {}) => {
103
+ const { input, ...rest } = options;
104
+ return execaJobBase(file, args, {
105
+ cancelSignal: jobScope.getStore(),
106
+ ...(input !== undefined ? { input } : { stdin: "ignore" }),
107
+ ...rest
108
+ });
109
+ };
110
+ /* the cancellation of the operation of a job currently run (if any, see "withinJobTimeout") */
111
+ export const jobSignal = () => jobScope.getStore();
112
+ export const execHook = async (file, args, options) => {
113
+ const result = await execaJobBase(file, args, {
114
+ cwd: options.cwd,
115
+ env: { GIT_TERMINAL_PROMPT: "0", ...options.env },
116
+ reject: false,
117
+ cancelSignal: options.signal,
118
+ ...(options.input !== undefined ? { input: options.input } : { stdin: "ignore" })
119
+ });
120
+ const code = typeof result.exitCode === "number" && !result.isTerminated && !result.isCanceled ? result.exitCode : -1;
121
+ return {
122
+ code,
123
+ stdout: String(result.stdout ?? ""),
124
+ stderr: String(result.stderr ?? "") || (code < 0 ? result.shortMessage ?? "" : "")
125
+ };
126
+ };
127
+ /* run a shell command of the preparing or the finishing of a job in the working directory
128
+ <cwd> with the additional environment variables <env> (like "execaJob"), logging its
129
+ output lines, and throwing on its failure */
130
+ export const shellJob = async (cmd, cwd, env, log) => {
131
+ await log("info", `running: ${cmd}`);
132
+ const result = await execaJobBase(cmd, {
133
+ shell: true, cwd, env, all: true, reject: false, stdin: "ignore", cancelSignal: jobScope.getStore()
134
+ });
135
+ for (const line of stripVTControlCharacters(result.all ?? "").split("\n"))
136
+ if (line.trim() !== "")
137
+ await log("info", line);
138
+ if (result.failed)
139
+ throw new Error(`agent: command "${cmd}" failed: ${result.exitCode !== undefined ?
140
+ `exit code ${result.exitCode}` : result.shortMessage}`);
141
+ };
142
+ /* run an operation (e.g. the preparing) of a job within its fixed maximum duration,
143
+ where the commands of the operation are killed on timeout (see "jobScope") and the
144
+ operation itself (which still has to end) is passed on timeout to <late> (if given),
145
+ so the resources it still creates afterwards can be released */
146
+ export const withinJobTimeout = async (what, op, late) => {
147
+ let timer;
148
+ const controller = new AbortController();
149
+ const pending = jobScope.run(controller.signal, op);
150
+ try {
151
+ return await Promise.race([
152
+ pending,
153
+ new Promise((_resolve, reject) => {
154
+ timer = setTimeout(() => {
155
+ controller.abort();
156
+ late?.(pending);
157
+ reject(new Error(`agent: ${what} timed out after ${jobTimeout / 1000}s`));
158
+ }, jobTimeout);
159
+ })
160
+ ]);
161
+ }
162
+ finally {
163
+ clearTimeout(timer);
164
+ }
165
+ };
166
+ /* whether the current working directory is inside a Git working copy */
167
+ export const gitInside = async () => (await execaJob("git", ["rev-parse", "--show-toplevel"], { reject: false })).exitCode === 0;
168
+ /* run Git operations changing the shared state of a Git repository (its references and its
169
+ worktrees) in mutual exclusion per repository in this process, identified by the common Git
170
+ directory of its working copy <root>, so tasks processed concurrently never collide on the
171
+ locks of Git (e.g. "index.lock" or a reference lock), where the operations must not nest */
172
+ export const gitLocked = async (root, op) => {
173
+ const { stdout } = await execaJob("git", ["rev-parse", "--git-common-dir"], { cwd: root });
174
+ return exclusive(`git:${await fs.promises.realpath(path.resolve(root, stdout.trim()))}`, op);
175
+ };
176
+ /* the Git worktree directory of the repository of the working copy <root>
177
+ in which a branch is checked out, or empty if none */
178
+ export const gitWorktree = async (root, branch) => {
179
+ const { stdout } = await execaJob("git", ["worktree", "list", "--porcelain"], { cwd: root });
180
+ let dir = "";
181
+ for (const line of stdout.split("\n")) {
182
+ if (line.startsWith("worktree "))
183
+ dir = line.slice("worktree ".length);
184
+ else if (line === `branch refs/heads/${branch}`)
185
+ return dir;
186
+ }
187
+ return "";
188
+ };
189
+ /* forcibly remove a Git worktree (if any) of the repository of the working copy <root>
190
+ (without the mutual exclusion of the repository, so it can be used within one) */
191
+ export const worktreeRemove = async (root, dir) => {
192
+ await execaJob("git", ["worktree", "remove", "--force", dir], { cwd: root, reject: false });
193
+ await execaJob("git", ["worktree", "prune"], { cwd: root, reject: false });
194
+ };
195
+ /* forcibly remove a Git worktree (if any) of the repository of the working copy <root>, e.g. a
196
+ temporary one or the leftover of an earlier processing which stopped before its cleanup */
197
+ export const gitWorktreeRemove = async (root, dir) => gitLocked(root, () => worktreeRemove(root, dir));
198
+ /* create a Git worktree of the repository of the working copy <root> with a branch
199
+ checked out, where a still missing branch is created from the start point <start> (if given) */
200
+ export const gitWorktreeAdd = async (root, dir, branch, start) => gitLocked(root, async () => {
201
+ const exists = (await execaJob("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: root, reject: false })).exitCode === 0;
202
+ const args = exists || start === undefined ? [dir, branch] : ["-b", branch, dir, start];
203
+ await worktreeRemove(root, dir);
204
+ await execaJob("git", ["worktree", "add", ...args], { cwd: root });
205
+ });
206
+ /* create a temporary Git worktree of the repository of the working copy <root>,
207
+ detached at the commit <commit> (replacing a leftover one) */
208
+ export const gitWorktreeDetached = async (root, dir, commit) => gitLocked(root, async () => {
209
+ await worktreeRemove(root, dir);
210
+ await execaJob("git", ["worktree", "add", "--quiet", "--detach", dir, commit], { cwd: root });
211
+ });
212
+ /* the id of the ASE worktree of a task (also used by the skills via "--worktree"),
213
+ as a task id can carry "#" (e.g. "#12" of the "seq:#%d" id scheme of the issue
214
+ tracker task stores), which an ASE worktree id (and a branch name derived from
215
+ it) cannot, so it is replaced by "_" (e.g. "_12") */
216
+ export const worktreeId = (id) => id.replace(/#/g, "_");
217
+ /* the safe name of a task id (for the placeholder "{id}" of the change set flow values) or
218
+ of a branch (for the ASE worktree of a "branch:" changeset), like by the skills: every
219
+ character other than "A-Za-z0-9_-" replaced by "-", runs of "-" reduced to one, and
220
+ leading and trailing "-" and "_" removed (e.g. "12" for "#12", "a-b" for "a/b") */
221
+ export const safeName = (name) => name.replace(/[^A-Za-z0-9_-]/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "");
222
+ /* the context worktree holding the uncommitted change set of a "worktree"
223
+ changeset: the ASE worktree named after the task if registered (as created
224
+ by the parallel implementer), else the working copy <root> */
225
+ export const contextDir = async (root, task) => {
226
+ const dir = Worktree.dir(worktreeId(task.id), false, false, root);
227
+ const { stdout } = await execaJob("git", ["worktree", "list", "--porcelain"], { cwd: root });
228
+ return stdout.split("\n").includes(`worktree ${dir}`) ? dir : root;
229
+ };
230
+ /* the ASE worktree id of a working copy, as selected by the "--worktree" option of the
231
+ skills as their context worktree: empty for the working copy <root>, else the id of
232
+ its ASE worktree */
233
+ export const worktreeName = (root, dir) => {
234
+ if (dir === root)
235
+ return "";
236
+ else if (path.dirname(dir) === Worktree.baseDir(false, false, root))
237
+ return path.basename(dir);
238
+ throw new Error(`agent: working copy "${dir}" is neither "${root}" nor an ASE worktree of it`);
239
+ };
240
+ /* whether the Git working copy <dir> carries no uncommitted changes (except the ASE state) */
241
+ export const gitClean = async (dir) => {
242
+ const { stdout } = await execaJob("git", ["status", "--porcelain", "--", ".", ":(exclude).ase"], { cwd: dir });
243
+ return stdout.trim() === "";
244
+ };
245
+ /* the text-only unified diff of a change set in the Git working copy <dir> (always without
246
+ the ASE state), from the fork point of its head from the commit <base> on, where the head
247
+ is the commit <head>, or, if empty, the working tree of <dir>, including its untracked
248
+ files, which are captured through a temporary index, so the index of <dir> stays untouched,
249
+ resolving to the fork point and the diff (empty if there is no change set) */
250
+ export const gitChangesetDiff = async (dir, base, head = "") => {
251
+ const { stdout: fork } = await execaJob("git", ["merge-base", base, head !== "" ? head : "HEAD"], { cwd: dir });
252
+ const from = fork.trim();
253
+ const args = ["--no-color", "--no-ext-diff"];
254
+ const paths = ["--", ".", ":(exclude).ase"];
255
+ let diff;
256
+ if (head !== "")
257
+ diff = (await execaJob("git", ["diff", ...args, from, head, ...paths], { cwd: dir })).stdout;
258
+ else {
259
+ const { stdout: index } = await execaJob("git", ["rev-parse", "--path-format=absolute", "--git-path", "index"], { cwd: dir });
260
+ const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ase-agent-index-"));
261
+ try {
262
+ const env = { GIT_INDEX_FILE: path.join(temp, "index") };
263
+ await fs.promises.copyFile(index.trim(), env.GIT_INDEX_FILE).catch((err) => {
264
+ if (err.code !== "ENOENT")
265
+ throw err;
266
+ });
267
+ await execaJob("git", ["add", "--all", ...paths], { cwd: dir, env });
268
+ diff = (await execaJob("git", ["diff", "--cached", ...args, from, ...paths], { cwd: dir, env })).stdout;
269
+ }
270
+ finally {
271
+ await fs.promises.rm(temp, { recursive: true, force: true });
272
+ }
273
+ }
274
+ return { base: from, diff: diff !== "" && !diff.endsWith("\n") ? `${diff}\n` : diff };
275
+ };