@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
package/dst/ase-hook.js CHANGED
@@ -9,9 +9,13 @@ import os from "node:os";
9
9
  import { execaSync } from "execa";
10
10
  import { quote } from "shell-quote";
11
11
  import * as v from "valibot";
12
- import Version from "./ase-version.js";
13
- import { Config, configSchema, parseScope } from "./ase-config.js";
14
- import { readStdin, writeStdout } from "./ase-stdio.js";
12
+ import Version from "./ase-lib-version.js";
13
+ import { Config, sessionState } from "./ase-config-core.js";
14
+ import { configSchema } from "./ase-config-schema.js";
15
+ import { parseScope, userStateDir } from "./ase-config-scope.js";
16
+ import { readStdin, writeStdout } from "./ase-lib-stdio.js";
17
+ import { Task } from "./ase-task.js";
18
+ import * as TaskFormat from "./ase-task-format.js";
15
19
  const addonMcpServers = [
16
20
  "chat-alibaba-qwen",
17
21
  "chat-deepseek",
@@ -88,12 +92,13 @@ export default class HookCommand {
88
92
  }
89
93
  /* resolve the base directory holding all per-session state */
90
94
  sessionBaseDir() {
91
- return path.join(os.homedir(), ".ase", "session");
95
+ return path.join(userStateDir(), "session");
92
96
  }
93
97
  /* garbage-collect orphaned session directories left behind by agents
94
98
  which died before their session-end hook could run; a live session
95
- keeps its directory's mtime current, as every tool call acquires a
96
- lock file inside it, so plain age is a reliable liveness signal */
99
+ usually refreshes its directory's mtime, as its locked configuration
100
+ accesses create lock files inside it, so plain age is a sufficient
101
+ liveness heuristic */
97
102
  pruneStaleSessions(currentSessionId) {
98
103
  const base = this.sessionBaseDir();
99
104
  let entries;
@@ -198,42 +203,20 @@ export default class HookCommand {
198
203
  return "";
199
204
  }
200
205
  }
201
- /* handler for "ase hook session-start" (all tools) */
202
- async doSessionStart(tool) {
203
- /* determine plugin root (env var name differs per tool) */
204
- const pluginRoot = this.pluginRoot(tool);
205
- /* determine path to external files */
206
- const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
207
- const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
208
- const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
209
- /* read external files */
210
- let pkg;
211
- let md;
212
- let style;
206
+ /* read a plugin file, throwing a descriptive error on failure */
207
+ readPluginFile(file, what) {
213
208
  try {
214
- pkg = fs.readFileSync(filePkg, "utf8");
209
+ return fs.readFileSync(file, "utf8");
215
210
  }
216
211
  catch (err) {
217
- throw new Error(`failed to read plugin manifest: ${filePkg}`, { cause: err });
218
- }
219
- try {
220
- md = fs.readFileSync(fileMd, "utf8");
212
+ throw new Error(`failed to read ${what}: ${file}`, { cause: err });
221
213
  }
222
- catch (err) {
223
- throw new Error(`failed to read constitution file: ${fileMd}`, { cause: err });
224
- }
225
- try {
226
- style = fs.readFileSync(fileStyle, "utf8");
227
- }
228
- catch (err) {
229
- throw new Error(`failed to read output style file: ${fileStyle}`, { cause: err });
230
- }
231
- /* determine own version */
232
- const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
233
- const versionCurrentPlugin = pkgObj.version ?? "";
214
+ }
215
+ /* determine the version hint on a tool/plugin version mismatch,
216
+ an available tool update, and a development setup */
217
+ async versionHint(versionCurrentPlugin) {
234
218
  const versionCurrentTool = Version.current();
235
219
  const versionLatestTool = await Version.latest();
236
- /* sanity check situation */
237
220
  const versionHints = [];
238
221
  if (versionCurrentPlugin !== versionCurrentTool)
239
222
  versionHints.push("**WARNING:** version *mismatch*: " +
@@ -242,7 +225,81 @@ export default class HookCommand {
242
225
  versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
243
226
  if (process.env.ASE_SETUP_DEV !== undefined)
244
227
  versionHints.push("**NOTICE:** *development* setup");
245
- const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
228
+ return versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
229
+ }
230
+ /* switch to the project directory announced in the event payload: GitHub
231
+ Copilot CLI runs its hooks in the plugin installation directory, so
232
+ every working-directory-relative lookup -- most notably the layered
233
+ configuration files -- would otherwise miss the project entirely */
234
+ switchDir(dir) {
235
+ if (dir === undefined || dir === "")
236
+ return;
237
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
238
+ this.log.write("warning", `hook: ignoring invalid project directory: ${dir}`);
239
+ return;
240
+ }
241
+ process.chdir(dir);
242
+ }
243
+ /* determine the project directory: the Git top-level directory of "cwd",
244
+ or "cwd" itself when outside a Git working tree */
245
+ determineProjectDir(cwd) {
246
+ let projectDir = cwd;
247
+ try {
248
+ const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
249
+ stderr: "ignore", cwd
250
+ });
251
+ if (result.stdout.trim() !== "")
252
+ projectDir = result.stdout.trim();
253
+ }
254
+ catch {
255
+ /* not inside a Git working tree */
256
+ }
257
+ return projectDir;
258
+ }
259
+ /* determine the project id: the valid configured "project.id", or else
260
+ the id derived from the already resolved project directory */
261
+ determineProjectId(projectDir, cfg) {
262
+ let configuredId = String(cfg.get("project.id") ?? "");
263
+ if (configuredId !== "" && !TaskFormat.ID_RE.test(configuredId)) {
264
+ this.log.write("warning", `hook: ignoring invalid configured "project.id" "${configuredId}" ` +
265
+ "(must match [A-Za-z0-9_-]+)");
266
+ configuredId = "";
267
+ }
268
+ return configuredId || Task.projectIdOf(projectDir);
269
+ }
270
+ /* append environment variable exports to the environment file of
271
+ Anthropic Claude Code CLI (the other tools have no equivalent mechanism) */
272
+ writeEnvFile(tool, vars) {
273
+ const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
274
+ if (envFile === "")
275
+ return;
276
+ const script = Object.entries(vars)
277
+ .map(([name, value]) => `export ${name}=${quote([value])}\n`)
278
+ .join("");
279
+ try {
280
+ fs.appendFileSync(envFile, script, "utf8");
281
+ }
282
+ catch (err) {
283
+ const message = err instanceof Error ? err.message : String(err);
284
+ this.log.write("warning", `hook: failed to write environment file: ${message}`);
285
+ }
286
+ }
287
+ /* handler for "ase hook session-start" (all tools) */
288
+ async doSessionStart(tool) {
289
+ /* determine plugin root (env var name differs per tool) */
290
+ const pluginRoot = this.pluginRoot(tool);
291
+ /* determine path to external files */
292
+ const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
293
+ const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
294
+ const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
295
+ /* read external files */
296
+ const pkg = this.readPluginFile(filePkg, "plugin manifest");
297
+ const md = this.readPluginFile(fileMd, "constitution file");
298
+ const style = this.readPluginFile(fileStyle, "output style file");
299
+ /* determine own version */
300
+ const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
301
+ const versionCurrentPlugin = pkgObj.version ?? "";
302
+ const versionHint = await this.versionHint(versionCurrentPlugin);
246
303
  /* read session information (Anthropic Claude Code CLI uses snake_case fields,
247
304
  GitHub Copilot CLI uses camelCase fields) */
248
305
  const stdin = await readStdin().catch(() => "");
@@ -253,36 +310,48 @@ export default class HookCommand {
253
310
  }));
254
311
  /* determine session id */
255
312
  const sessionId = this.pickSessionId(input);
313
+ /* adopt the project directory before any context is derived from the
314
+ working directory, especially the layered configuration below
315
+ (GitHub Copilot CLI only -- the other tools already run their
316
+ hooks inside the project directory) */
317
+ if (tool === "copilot")
318
+ this.switchDir(input.cwd);
256
319
  /* garbage-collect orphaned session directories of previous agent runs */
257
320
  this.pruneStaleSessions(sessionId);
258
321
  /* establish config context (session-scoped only if a valid sessionId is present) */
259
322
  const hasSession = this.isValidSessionId(sessionId);
260
323
  const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
261
- /* determine task id (only persist when scoped to a real session) */
324
+ /* determine the project directory, persisted below, because for GitHub Copilot
325
+ CLI it is the single piece of context this agent tool withholds from the MCP
326
+ server: it starts the MCP server in the plugin installation directory, long
327
+ before this hook runs, so the server can only learn it from here (and for all
328
+ agent tools, the project service determines the sessions of its project by it,
329
+ in order to attach them to the usage windows of their tasks assigned elsewhere) */
330
+ const projectDir = this.determineProjectDir(input.cwd ?? process.cwd());
331
+ cfg.read();
332
+ /* determine headless mode */
333
+ const headless = process.env.ASE_HEADLESS === "true" ? "true" : "false";
334
+ /* determine task id and persist it, together with the project directory
335
+ (see above) and the headless mode (as the project service never attaches
336
+ the headless sessions of the agents, which book their usage themselves, to
337
+ usage windows), into the internal session state (only when scoped to a real
338
+ session), normalizing the path separators of the project directory, as Git
339
+ reports the top-level directory with forward slashes even on Windows */
262
340
  const taskId = process.env.ASE_TASK_ID ?? "default";
263
- cfg.lock(() => {
264
- cfg.read();
265
- if (hasSession) {
266
- cfg.set("agent.task", taskId);
267
- cfg.write();
268
- }
269
- });
341
+ if (hasSession) {
342
+ const state = sessionState(this.log, sessionId);
343
+ state.lock(() => {
344
+ state.read();
345
+ state.set("task", taskId);
346
+ state.set("basedir", path.resolve(projectDir));
347
+ state.set("headless", headless);
348
+ state.write();
349
+ });
350
+ }
270
351
  /* initialize agent activity status */
271
352
  this.writeAgentStatus("ready");
272
353
  /* determine project id */
273
- const cwd = input.cwd ?? process.cwd();
274
- let projectDir = cwd;
275
- try {
276
- const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
277
- stderr: "ignore", cwd
278
- });
279
- if (result.stdout.trim() !== "")
280
- projectDir = result.stdout.trim();
281
- }
282
- catch {
283
- /* not inside a Git working tree */
284
- }
285
- const projectId = path.basename(projectDir);
354
+ const projectId = this.determineProjectId(projectDir, cfg);
286
355
  /* determine user id */
287
356
  const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
288
357
  /* helper function: determine a setting from the explicitly configured
@@ -293,64 +362,100 @@ export default class HookCommand {
293
362
  const val = cfg.getExplicit(key);
294
363
  return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
295
364
  };
296
- /* determine agent persona style, agent guidance level, and project boxing transparency */
365
+ /* determine agent persona style, agent guidance level, project
366
+ boxing transparency, and project task lifecycle */
297
367
  const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
298
368
  const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
299
369
  const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
370
+ const lifecycle = setting("project.task.lifecycle", "ASE_PROJECT_TASK_LIFECYCLE", "solo");
371
+ /* determine the defaults of the change set flow keys of the task plans */
372
+ const defaultSource = setting("project.task.default.source", "ASE_PROJECT_TASK_DEFAULT_SOURCE", "worktree");
373
+ const defaultChangeset = setting("project.task.default.changeset", "ASE_PROJECT_TASK_DEFAULT_CHANGESET", "worktree");
374
+ const defaultTarget = setting("project.task.default.target", "ASE_PROJECT_TASK_DEFAULT_TARGET", "source");
300
375
  /* determine the specification base directory and the whitespace-separated
301
376
  list of SpecBook schema configurations (empty: the bundled standard schema) */
302
377
  const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
303
378
  const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
304
- /* determine headless mode */
305
- const headless = process.env.ASE_HEADLESS === "true" ? "true" : "false";
379
+ /* publish the ASE session information and emit it as the hook output payload */
380
+ const payload = this.publishSessionInfo(tool, {
381
+ version: versionCurrentPlugin, versionHint, pluginRoot, persona, guidance, userId, projectId,
382
+ boxing, lifecycle, defaultSource, defaultChangeset, defaultTarget,
383
+ specBasedir, specSchema, taskId, sessionId, headless
384
+ }, md, path.dirname(fileMd), style);
385
+ await writeStdout(JSON.stringify(payload));
386
+ return 0;
387
+ }
388
+ /* handler for "ase hook session-persona" (all tools): inject the persona rules
389
+ as a separate hook output, as agent tools like Anthropic Claude Code CLI
390
+ persist any hook output beyond 10,000 characters to a file and only show a preview */
391
+ async doSessionPersona(tool) {
392
+ /* read persona file */
393
+ const pluginRoot = this.pluginRoot(tool);
394
+ const persona = this.readPluginFile(path.join(pluginRoot, "meta", "ase-persona.md"), "persona file");
395
+ /* inject markdown into session context (same nesting as for "session-start") */
396
+ const payload = tool !== "copilot" ? {
397
+ "hookSpecificOutput": {
398
+ "hookEventName": "SessionStart",
399
+ "additionalContext": persona
400
+ }
401
+ } : {
402
+ "additionalContext": persona
403
+ };
404
+ await writeStdout(JSON.stringify(payload));
405
+ return 0;
406
+ }
407
+ /* publish the ASE session information: export it to the shell commands and
408
+ render it, together with the constitution markdown, the output style, and
409
+ the deterministic ASE banner, into the "session-start" hook output payload */
410
+ publishSessionInfo(tool, info, md, mdDir, style) {
411
+ const { version, versionHint, pluginRoot, persona, guidance, userId, projectId, boxing, lifecycle, defaultSource, defaultChangeset, defaultTarget, specBasedir, specSchema, taskId, sessionId, headless } = info;
306
412
  /* provide ASE information to Anthropic Claude Code CLI shell commands
307
413
  (Anthropic Claude Code CLI only -- GitHub Copilot CLI has no equivalent mechanism) */
308
- const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
309
- if (envFile !== "") {
310
- const script = `export ASE_VERSION=${quote([versionCurrentPlugin])}\n` +
311
- `export ASE_PLUGIN_ROOT=${quote([pluginRoot])}\n` +
312
- `export ASE_USER_ID=${quote([userId])}\n` +
313
- `export ASE_PROJECT_ID=${quote([projectId])}\n` +
314
- `export ASE_PROJECT_BOXING=${quote([boxing])}\n` +
315
- `export ASE_SPEC_BASEDIR=${quote([specBasedir])}\n` +
316
- `export ASE_SPEC_SCHEMA=${quote([specSchema])}\n` +
317
- `export ASE_TASK_ID=${quote([taskId])}\n` +
318
- `export ASE_SESSION_ID=${quote([sessionId])}\n` +
319
- `export ASE_HEADLESS=${quote([headless])}\n` +
320
- `export ASE_AGENT_TOOL=${quote([tool])}\n`;
321
- try {
322
- fs.appendFileSync(envFile, script, "utf8");
323
- }
324
- catch (err) {
325
- const message = err instanceof Error ? err.message : String(err);
326
- this.log.write("warning", `hook: failed to write environment file: ${message}`);
327
- }
328
- }
414
+ this.writeEnvFile(tool, {
415
+ ASE_VERSION: version,
416
+ ASE_PLUGIN_ROOT: pluginRoot,
417
+ ASE_USER_ID: userId,
418
+ ASE_PROJECT_ID: projectId,
419
+ ASE_PROJECT_BOXING: boxing,
420
+ ASE_PROJECT_TASK_LIFECYCLE: lifecycle,
421
+ ASE_PROJECT_TASK_DEFAULT_SOURCE: defaultSource,
422
+ ASE_PROJECT_TASK_DEFAULT_CHANGESET: defaultChangeset,
423
+ ASE_PROJECT_TASK_DEFAULT_TARGET: defaultTarget,
424
+ ASE_SPEC_BASEDIR: specBasedir,
425
+ ASE_SPEC_SCHEMA: specSchema,
426
+ ASE_TASK_ID: taskId,
427
+ ASE_SESSION_ID: sessionId,
428
+ ASE_HEADLESS: headless,
429
+ ASE_AGENT_TOOL: tool
430
+ });
329
431
  /* prepend ASE information to constitution markdown */
330
- md =
331
- `<ase-version>${versionCurrentPlugin}</ase-version>\n` +
332
- `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
333
- `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
334
- `<ase-persona-style>${persona}</ase-persona-style>\n` +
335
- `<ase-guidance-level>${guidance}</ase-guidance-level>\n` +
336
- `<ase-user-id>${userId}</ase-user-id>\n` +
337
- `<ase-project-id>${projectId}</ase-project-id>\n` +
338
- `<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
339
- `<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
340
- `<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
341
- `<ase-task-id>${taskId}</ase-task-id>\n` +
342
- `<ase-session-id>${sessionId}</ase-session-id>\n` +
343
- `<ase-headless>${headless}</ase-headless>\n` +
344
- `<ase-agent-tool>${tool}</ase-agent-tool>\n` +
345
- "\n" + md;
432
+ let context = `<ase-version>${version}</ase-version>\n` +
433
+ `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
434
+ `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
435
+ `<ase-persona-style>${persona}</ase-persona-style>\n` +
436
+ `<ase-guidance-level>${guidance}</ase-guidance-level>\n` +
437
+ `<ase-user-id>${userId}</ase-user-id>\n` +
438
+ `<ase-project-id>${projectId}</ase-project-id>\n` +
439
+ `<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
440
+ `<ase-project-task-lifecycle>${lifecycle}</ase-project-task-lifecycle>\n` +
441
+ `<ase-project-task-default-source>${defaultSource}</ase-project-task-default-source>\n` +
442
+ `<ase-project-task-default-changeset>${defaultChangeset}</ase-project-task-default-changeset>\n` +
443
+ `<ase-project-task-default-target>${defaultTarget}</ase-project-task-default-target>\n` +
444
+ `<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
445
+ `<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
446
+ `<ase-task-id>${taskId}</ase-task-id>\n` +
447
+ `<ase-session-id>${sessionId}</ase-session-id>\n` +
448
+ `<ase-headless>${headless}</ase-headless>\n` +
449
+ `<ase-agent-tool>${tool}</ase-agent-tool>\n` +
450
+ "\n" + md;
346
451
  /* expand all @<file> references manually */
347
- md = this.expandReferences(md, path.dirname(fileMd));
452
+ context = this.expandReferences(context, mdDir);
348
453
  /* append the output style to the constitution markdown (GitHub
349
454
  Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
350
455
  applies the plugin output style natively), stripping the
351
456
  YAML frontmatter which only Anthropic Claude Code CLI understands */
352
457
  if (tool !== "claude")
353
- md += "\n" + style.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
458
+ context += "\n" + style.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
354
459
  /* build the deterministic ASE banner (rendered directly by the
355
460
  agent harness, independent of any model decision, so it is
356
461
  guaranteed to appear once in every non-headless session;
@@ -358,7 +463,7 @@ export default class HookCommand {
358
463
  "systemMessage" field for this -- GitHub Copilot CLI has no equivalent);
359
464
  the trailing help hint is emitted only if the guidance level asks for it */
360
465
  const banner = "\n" +
361
- `\n⧉ ASE: ⎈ version: ${versionCurrentPlugin}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
466
+ `\n⧉ ASE: ⎈ version: ${version}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
362
467
  `\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
363
468
  `\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
364
469
  `\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
@@ -373,10 +478,10 @@ export default class HookCommand {
373
478
  const payload = tool !== "copilot" ? {
374
479
  "hookSpecificOutput": {
375
480
  "hookEventName": "SessionStart",
376
- "additionalContext": md
481
+ "additionalContext": context
377
482
  }
378
483
  } : {
379
- "additionalContext": md
484
+ "additionalContext": context
380
485
  };
381
486
  /* attach the deterministic banner as a top-level "systemMessage"
382
487
  (only for the harnesses that support it and only when not
@@ -385,8 +490,7 @@ export default class HookCommand {
385
490
  by letting the model emit the banner itself) */
386
491
  if ((tool === "claude" || tool === "codex") && headless !== "true" && guidance !== "none")
387
492
  payload.systemMessage = banner;
388
- await writeStdout(JSON.stringify(payload));
389
- return 0;
493
+ return payload;
390
494
  }
391
495
  /* publish the agent activity marker to tmux as a per-pane user
392
496
  option, so tmux can render the live state via
@@ -420,7 +524,7 @@ export default class HookCommand {
420
524
  async doSessionEnd(_tool) {
421
525
  /* determine session id */
422
526
  const sessionId = await this.readSessionIdFromStdin();
423
- /* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
527
+ /* remove the session directory <state-dir>/session/<id> (only for a valid sessionId) */
424
528
  if (this.isValidSessionId(sessionId)) {
425
529
  const dir = path.join(this.sessionBaseDir(), sessionId);
426
530
  try {
@@ -429,6 +533,18 @@ export default class HookCommand {
429
533
  catch (_e) {
430
534
  /* best-effort: ignore failures */
431
535
  }
536
+ /* detach the session from its AI usage window (if any), which closes
537
+ and books the window once no other session remains attached to it
538
+ (noting the session end and starting the owning project service in the
539
+ background, if it is not reachable in time, and imported dynamically
540
+ to keep it out of all other hooks) */
541
+ try {
542
+ const { UsageTracker } = await import("./ase-service-otel.js");
543
+ await UsageTracker.detachRemote(sessionId);
544
+ }
545
+ catch (err) {
546
+ this.log.write("warning", `hook: failed to detach session from its usage window: ${err instanceof Error ? err.message : String(err)}`);
547
+ }
432
548
  }
433
549
  return 0;
434
550
  }
@@ -446,16 +562,16 @@ export default class HookCommand {
446
562
  }));
447
563
  return this.pickSessionId(input);
448
564
  }
449
- /* read the session-scoped "agent.skill" config value */
565
+ /* read the "skill" value of the internal session state */
450
566
  readActiveSkill(sessionId) {
451
567
  if (!this.isValidSessionId(sessionId))
452
568
  return "";
453
569
  try {
454
- const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
570
+ const state = sessionState(this.log, sessionId);
455
571
  let val = "";
456
- cfg.lock(() => {
457
- cfg.read();
458
- const skill = cfg.get("agent.skill");
572
+ state.lock(() => {
573
+ state.read();
574
+ const skill = state.get("skill");
459
575
  if (typeof skill === "string")
460
576
  val = skill;
461
577
  });
@@ -614,6 +730,7 @@ export default class HookCommand {
614
730
  /* register CLI sub-commands "ase hook <event>" */
615
731
  const subCmds = [
616
732
  { name: "session-start", desc: "handle SessionStart hook event", handler: (tool) => this.doSessionStart(tool) },
733
+ { name: "session-persona", desc: "handle SessionStart hook event (persona)", handler: (tool) => this.doSessionPersona(tool) },
617
734
  { name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
618
735
  { name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
619
736
  { name: "permission-request", desc: "handle tool PermissionRequest hook event", handler: (tool) => this.doPermissionRequest(tool) },
@@ -0,0 +1,130 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { spawn } from "node:child_process";
10
+ import { Service } from "./ase-service.js";
11
+ /* the CLI command group managing a long-running process (a server, an agent, or a fleet
12
+ of them) in the background or in the foreground: it registers the command group, spawns
13
+ the process detached into its log file, awaits its startup, checks whether it is alive,
14
+ shows its log file, and stops it, where the concrete command decides on its configuration and runtime state */
15
+ export class DaemonCommand {
16
+ log;
17
+ constructor(log) {
18
+ this.log = log;
19
+ }
20
+ /* register the CLI sub-command group below the CLI command <parent>,
21
+ which shows its help on its own (as it requires a sub-command) */
22
+ group(parent) {
23
+ const group = parent
24
+ .command(this.words.split(" ").at(-1))
25
+ .description(this.description)
26
+ .action(() => {
27
+ group.outputHelp();
28
+ process.exit(1);
29
+ });
30
+ return group;
31
+ }
32
+ /* register the CLI sub-command "log" below the CLI sub-command group <group>, showing the
33
+ log file (resolved by <logFile> from the options of the sub-command) of the process run
34
+ in the background, where <setup> adds further options to the sub-command and each shown
35
+ line is passed through <transform> (e.g. to colorize parts of it) */
36
+ registerLog(group, logFile, setup = () => { }, transform) {
37
+ const cmd = group
38
+ .command("log")
39
+ .description(`Show the log of the ${this.label} run in the background`)
40
+ .option("-f, --follow", "keep following the log for newly appended lines")
41
+ .option("-n, --lines <n>", "show the last <n> lines only (default: all, or 10 with --follow)");
42
+ setup(cmd);
43
+ cmd.action(async (opts) => {
44
+ const file = logFile(opts);
45
+ if (!fs.existsSync(file)) {
46
+ this.log.write("info", `${this.label}: no log file "${file}" ` +
47
+ "(not yet run in the background, as one run in the foreground logs to the terminal)");
48
+ return;
49
+ }
50
+ process.exitCode = await Service.showLog(file, opts, transform);
51
+ });
52
+ }
53
+ /* spawn the CLI with the arguments <args> detached, with the additional
54
+ environment <env>, logging into the (trimmed) log file <logFile> */
55
+ spawnDetached(args, env, logFile) {
56
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
57
+ Service.trimLog(logFile);
58
+ const fd = fs.openSync(logFile, "a");
59
+ const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
60
+ const child = spawn(process.execPath, [entry, ...args], {
61
+ detached: true,
62
+ env: { ...process.env, ...env },
63
+ stdio: ["ignore", fd, fd]
64
+ });
65
+ fs.closeSync(fd);
66
+ return child;
67
+ }
68
+ /* await the readiness of a child process (checked every 100ms up to <timeout> milliseconds),
69
+ resolving to null once ready, else to the reason of its failure, like "exited during
70
+ startup (code 1)" (where a child process not ready within the timeout is terminated) */
71
+ async awaitChild(child, ready, timeout) {
72
+ let exited = false;
73
+ let exitCode = null;
74
+ child.once("exit", (code) => {
75
+ exited = true;
76
+ exitCode = code;
77
+ });
78
+ for (let waited = 0; waited < timeout; waited += 100) {
79
+ await new Promise((resolve) => setTimeout(resolve, 100));
80
+ if (exited)
81
+ break;
82
+ if (ready())
83
+ return null;
84
+ }
85
+ if (!exited)
86
+ child.kill("SIGTERM");
87
+ return exited ?
88
+ `exited during startup (code ${exitCode})` :
89
+ "failed to start within timeout";
90
+ }
91
+ /* await the readiness of the detached process, else fail with the tail of its log file */
92
+ async awaitDetached(child, logFile, ready, timeout) {
93
+ const failure = await this.awaitChild(child, ready, timeout);
94
+ child.unref();
95
+ if (failure === null)
96
+ return;
97
+ const reason = `${this.label}: ${failure}`;
98
+ const tail = Service.readLogTail(logFile, 20);
99
+ throw new Error(tail.length > 0 ? `${reason}\n---- ${logFile} (tail) ----\n${tail}` : reason);
100
+ }
101
+ /* check whether a process is still alive */
102
+ isAlive(pid) {
103
+ try {
104
+ process.kill(pid, 0);
105
+ return true;
106
+ }
107
+ catch (err) {
108
+ return err.code === "EPERM";
109
+ }
110
+ }
111
+ /* terminate a process and wait (up to <timeout> milliseconds) until it is gone,
112
+ resolving to false if it was already gone */
113
+ async terminate(pid, timeout) {
114
+ try {
115
+ process.kill(pid, "SIGTERM");
116
+ }
117
+ catch (err) {
118
+ const e = err;
119
+ if (e.code !== "ESRCH")
120
+ throw err;
121
+ return false;
122
+ }
123
+ for (let waited = 0; waited < timeout; waited += 100) {
124
+ await new Promise((resolve) => setTimeout(resolve, 100));
125
+ if (!this.isAlive(pid))
126
+ return true;
127
+ }
128
+ throw new Error(`${this.label}: process ${pid} did not stop within timeout`);
129
+ }
130
+ }
@@ -0,0 +1,20 @@
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 { createHash } from "node:crypto";
7
+ /* the alphabet and length of the NanoId-style hashes */
8
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
9
+ const NANOID = 10;
10
+ /* derive the short NanoId-style hash of a hint: the SHA-1 digest of
11
+ the hint, base62-encoded and truncated to its lowest digits */
12
+ export const nanoid = (hint) => {
13
+ let n = BigInt(`0x${createHash("sha1").update(hint).digest("hex")}`);
14
+ let id = "";
15
+ for (let i = 0; i < NANOID; i++) {
16
+ id += BASE62[Number(n % 62n)];
17
+ n /= 62n;
18
+ }
19
+ return id;
20
+ };