@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
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",
@@ -37,7 +41,6 @@ const toolSpecs = {
37
41
  "claude": {
38
42
  toolNameField: "tool_name",
39
43
  toolInputField: "tool_input",
40
- toolInputIsString: false,
41
44
  bashToolName: "Bash",
42
45
  mcpToolNamePattern: /^mcp__plugin_ase_ase__.+/,
43
46
  addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
@@ -48,7 +51,6 @@ const toolSpecs = {
48
51
  "copilot": {
49
52
  toolNameField: "toolName",
50
53
  toolInputField: "toolArgs",
51
- toolInputIsString: true,
52
54
  bashToolName: "bash",
53
55
  mcpToolNamePattern: /^ase-.+/,
54
56
  addonMcpToolNamePattern: addonMcpToolNamePattern("", "-.+"),
@@ -59,7 +61,6 @@ const toolSpecs = {
59
61
  "codex": {
60
62
  toolNameField: "tool_name",
61
63
  toolInputField: "tool_input",
62
- toolInputIsString: false,
63
64
  bashToolName: "Bash",
64
65
  mcpToolNamePattern: /^mcp__ase__.+/,
65
66
  addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
@@ -91,12 +92,13 @@ export default class HookCommand {
91
92
  }
92
93
  /* resolve the base directory holding all per-session state */
93
94
  sessionBaseDir() {
94
- return path.join(os.homedir(), ".ase", "session");
95
+ return path.join(userStateDir(), "session");
95
96
  }
96
97
  /* garbage-collect orphaned session directories left behind by agents
97
98
  which died before their session-end hook could run; a live session
98
- keeps its directory's mtime current, as every tool call acquires a
99
- 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 */
100
102
  pruneStaleSessions(currentSessionId) {
101
103
  const base = this.sessionBaseDir();
102
104
  let entries;
@@ -201,42 +203,20 @@ export default class HookCommand {
201
203
  return "";
202
204
  }
203
205
  }
204
- /* handler for "ase hook session-start" (all tools) */
205
- async doSessionStart(tool) {
206
- /* determine plugin root (env var name differs per tool) */
207
- const pluginRoot = this.pluginRoot(tool);
208
- /* determine path to external files */
209
- const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
210
- const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
211
- const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
212
- /* read external files */
213
- let pkg;
214
- let md;
215
- let style;
206
+ /* read a plugin file, throwing a descriptive error on failure */
207
+ readPluginFile(file, what) {
216
208
  try {
217
- pkg = fs.readFileSync(filePkg, "utf8");
209
+ return fs.readFileSync(file, "utf8");
218
210
  }
219
211
  catch (err) {
220
- throw new Error(`failed to read plugin manifest: ${filePkg}`, { cause: err });
221
- }
222
- try {
223
- md = fs.readFileSync(fileMd, "utf8");
212
+ throw new Error(`failed to read ${what}: ${file}`, { cause: err });
224
213
  }
225
- catch (err) {
226
- throw new Error(`failed to read constitution file: ${fileMd}`, { cause: err });
227
- }
228
- try {
229
- style = fs.readFileSync(fileStyle, "utf8");
230
- }
231
- catch (err) {
232
- throw new Error(`failed to read output style file: ${fileStyle}`, { cause: err });
233
- }
234
- /* determine own version */
235
- const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
236
- 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) {
237
218
  const versionCurrentTool = Version.current();
238
219
  const versionLatestTool = await Version.latest();
239
- /* sanity check situation */
240
220
  const versionHints = [];
241
221
  if (versionCurrentPlugin !== versionCurrentTool)
242
222
  versionHints.push("**WARNING:** version *mismatch*: " +
@@ -245,7 +225,81 @@ export default class HookCommand {
245
225
  versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
246
226
  if (process.env.ASE_SETUP_DEV !== undefined)
247
227
  versionHints.push("**NOTICE:** *development* setup");
248
- 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);
249
303
  /* read session information (Anthropic Claude Code CLI uses snake_case fields,
250
304
  GitHub Copilot CLI uses camelCase fields) */
251
305
  const stdin = await readStdin().catch(() => "");
@@ -256,36 +310,48 @@ export default class HookCommand {
256
310
  }));
257
311
  /* determine session id */
258
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);
259
319
  /* garbage-collect orphaned session directories of previous agent runs */
260
320
  this.pruneStaleSessions(sessionId);
261
321
  /* establish config context (session-scoped only if a valid sessionId is present) */
262
322
  const hasSession = this.isValidSessionId(sessionId);
263
323
  const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
264
- /* 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 */
265
340
  const taskId = process.env.ASE_TASK_ID ?? "default";
266
- cfg.lock(() => {
267
- cfg.read();
268
- if (hasSession) {
269
- cfg.set("agent.task", taskId);
270
- cfg.write();
271
- }
272
- });
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
+ }
273
351
  /* initialize agent activity status */
274
352
  this.writeAgentStatus("ready");
275
353
  /* determine project id */
276
- const cwd = input.cwd ?? process.cwd();
277
- let projectDir = cwd;
278
- try {
279
- const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
280
- stderr: "ignore", cwd
281
- });
282
- if (result.stdout.trim() !== "")
283
- projectDir = result.stdout.trim();
284
- }
285
- catch {
286
- /* not inside a Git working tree */
287
- }
288
- const projectId = path.basename(projectDir);
354
+ const projectId = this.determineProjectId(projectDir, cfg);
289
355
  /* determine user id */
290
356
  const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
291
357
  /* helper function: determine a setting from the explicitly configured
@@ -296,64 +362,100 @@ export default class HookCommand {
296
362
  const val = cfg.getExplicit(key);
297
363
  return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
298
364
  };
299
- /* 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 */
300
367
  const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
301
368
  const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
302
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");
303
375
  /* determine the specification base directory and the whitespace-separated
304
376
  list of SpecBook schema configurations (empty: the bundled standard schema) */
305
377
  const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
306
378
  const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
307
- /* determine headless mode */
308
- 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;
309
412
  /* provide ASE information to Anthropic Claude Code CLI shell commands
310
413
  (Anthropic Claude Code CLI only -- GitHub Copilot CLI has no equivalent mechanism) */
311
- const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
312
- if (envFile !== "") {
313
- const script = `export ASE_VERSION=${quote([versionCurrentPlugin])}\n` +
314
- `export ASE_PLUGIN_ROOT=${quote([pluginRoot])}\n` +
315
- `export ASE_USER_ID=${quote([userId])}\n` +
316
- `export ASE_PROJECT_ID=${quote([projectId])}\n` +
317
- `export ASE_PROJECT_BOXING=${quote([boxing])}\n` +
318
- `export ASE_SPEC_BASEDIR=${quote([specBasedir])}\n` +
319
- `export ASE_SPEC_SCHEMA=${quote([specSchema])}\n` +
320
- `export ASE_TASK_ID=${quote([taskId])}\n` +
321
- `export ASE_SESSION_ID=${quote([sessionId])}\n` +
322
- `export ASE_HEADLESS=${quote([headless])}\n` +
323
- `export ASE_AGENT_TOOL=${quote([tool])}\n`;
324
- try {
325
- fs.appendFileSync(envFile, script, "utf8");
326
- }
327
- catch (err) {
328
- const message = err instanceof Error ? err.message : String(err);
329
- this.log.write("warning", `hook: failed to write environment file: ${message}`);
330
- }
331
- }
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
+ });
332
431
  /* prepend ASE information to constitution markdown */
333
- md =
334
- `<ase-version>${versionCurrentPlugin}</ase-version>\n` +
335
- `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
336
- `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
337
- `<ase-persona-style>${persona}</ase-persona-style>\n` +
338
- `<ase-guidance-level>${guidance}</ase-guidance-level>\n` +
339
- `<ase-user-id>${userId}</ase-user-id>\n` +
340
- `<ase-project-id>${projectId}</ase-project-id>\n` +
341
- `<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
342
- `<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
343
- `<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
344
- `<ase-task-id>${taskId}</ase-task-id>\n` +
345
- `<ase-session-id>${sessionId}</ase-session-id>\n` +
346
- `<ase-headless>${headless}</ase-headless>\n` +
347
- `<ase-agent-tool>${tool}</ase-agent-tool>\n` +
348
- "\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;
349
451
  /* expand all @<file> references manually */
350
- md = this.expandReferences(md, path.dirname(fileMd));
452
+ context = this.expandReferences(context, mdDir);
351
453
  /* append the output style to the constitution markdown (GitHub
352
454
  Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
353
455
  applies the plugin output style natively), stripping the
354
456
  YAML frontmatter which only Anthropic Claude Code CLI understands */
355
457
  if (tool !== "claude")
356
- 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/, "");
357
459
  /* build the deterministic ASE banner (rendered directly by the
358
460
  agent harness, independent of any model decision, so it is
359
461
  guaranteed to appear once in every non-headless session;
@@ -361,7 +463,7 @@ export default class HookCommand {
361
463
  "systemMessage" field for this -- GitHub Copilot CLI has no equivalent);
362
464
  the trailing help hint is emitted only if the guidance level asks for it */
363
465
  const banner = "\n" +
364
- `\n⧉ ASE: ⎈ version: ${versionCurrentPlugin}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
466
+ `\n⧉ ASE: ⎈ version: ${version}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
365
467
  `\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
366
468
  `\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
367
469
  `\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
@@ -376,10 +478,10 @@ export default class HookCommand {
376
478
  const payload = tool !== "copilot" ? {
377
479
  "hookSpecificOutput": {
378
480
  "hookEventName": "SessionStart",
379
- "additionalContext": md
481
+ "additionalContext": context
380
482
  }
381
483
  } : {
382
- "additionalContext": md
484
+ "additionalContext": context
383
485
  };
384
486
  /* attach the deterministic banner as a top-level "systemMessage"
385
487
  (only for the harnesses that support it and only when not
@@ -388,8 +490,7 @@ export default class HookCommand {
388
490
  by letting the model emit the banner itself) */
389
491
  if ((tool === "claude" || tool === "codex") && headless !== "true" && guidance !== "none")
390
492
  payload.systemMessage = banner;
391
- await writeStdout(JSON.stringify(payload));
392
- return 0;
493
+ return payload;
393
494
  }
394
495
  /* publish the agent activity marker to tmux as a per-pane user
395
496
  option, so tmux can render the live state via
@@ -423,7 +524,7 @@ export default class HookCommand {
423
524
  async doSessionEnd(_tool) {
424
525
  /* determine session id */
425
526
  const sessionId = await this.readSessionIdFromStdin();
426
- /* 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) */
427
528
  if (this.isValidSessionId(sessionId)) {
428
529
  const dir = path.join(this.sessionBaseDir(), sessionId);
429
530
  try {
@@ -432,6 +533,18 @@ export default class HookCommand {
432
533
  catch (_e) {
433
534
  /* best-effort: ignore failures */
434
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
+ }
435
548
  }
436
549
  return 0;
437
550
  }
@@ -449,16 +562,16 @@ export default class HookCommand {
449
562
  }));
450
563
  return this.pickSessionId(input);
451
564
  }
452
- /* read the session-scoped "agent.skill" config value */
565
+ /* read the "skill" value of the internal session state */
453
566
  readActiveSkill(sessionId) {
454
567
  if (!this.isValidSessionId(sessionId))
455
568
  return "";
456
569
  try {
457
- const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
570
+ const state = sessionState(this.log, sessionId);
458
571
  let val = "";
459
- cfg.lock(() => {
460
- cfg.read();
461
- const skill = cfg.get("agent.skill");
572
+ state.lock(() => {
573
+ state.read();
574
+ const skill = state.get("skill");
462
575
  if (typeof skill === "string")
463
576
  val = skill;
464
577
  });
@@ -498,9 +611,9 @@ export default class HookCommand {
498
611
  const rawInput = input[spec.toolInputField];
499
612
  const toolName = typeof rawName === "string" ? rawName : "";
500
613
  let toolInput = {};
501
- if (spec.toolInputIsString && typeof rawInput === "string")
614
+ if (typeof rawInput === "string")
502
615
  toolInput = this.parseJSON(rawInput, toolInputSchema);
503
- else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null) {
616
+ else if (typeof rawInput === "object" && rawInput !== null) {
504
617
  const result = v.safeParse(toolInputSchema, rawInput);
505
618
  if (result.success)
506
619
  toolInput = result.output;
@@ -617,6 +730,7 @@ export default class HookCommand {
617
730
  /* register CLI sub-commands "ase hook <event>" */
618
731
  const subCmds = [
619
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) },
620
734
  { name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
621
735
  { name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
622
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
+ };