@rse/ase 2.0.2 → 2.0.4

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 (236) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +2 -0
  12. package/dst/ase-hook.js +13 -13
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  15. package/dst/ase-lib-version.js +14 -5
  16. package/dst/ase-man-catalog.js +229 -0
  17. package/dst/ase-man-render.js +278 -0
  18. package/dst/ase-man-tui.js +636 -0
  19. package/dst/ase-man.js +33 -27
  20. package/dst/ase-mcp.js +4 -4
  21. package/dst/ase-service-getopt.js +2 -2
  22. package/dst/ase-service-otel.js +4 -4
  23. package/dst/ase-service-skills.js +5 -15
  24. package/dst/ase-service.js +87 -210
  25. package/dst/ase-setup-common.js +1 -2
  26. package/dst/ase-setup-settings.js +6 -9
  27. package/dst/ase-setup.js +4 -5
  28. package/dst/ase-spec.js +103 -20
  29. package/dst/ase-statusline.js +170 -170
  30. package/dst/ase-task-board-filter.js +5 -4
  31. package/dst/ase-task-board-flow.js +7 -6
  32. package/dst/ase-task-board-graph.js +5 -4
  33. package/dst/ase-task-board-tui-control.js +41 -35
  34. package/dst/ase-task-board-tui-model.js +185 -167
  35. package/dst/ase-task-board-tui-popup.js +2 -3
  36. package/dst/ase-task-board-tui-view.js +74 -70
  37. package/dst/ase-task-board-web-client.js +27 -27
  38. package/dst/ase-task-board-web-server.js +2 -2
  39. package/dst/ase-task-board.js +1 -1
  40. package/dst/ase-task-format.js +1 -1
  41. package/dst/ase-task-store-client.js +17 -5
  42. package/dst/ase-task-store-core.js +5 -7
  43. package/dst/ase-task-store-issues.js +16 -1
  44. package/dst/ase-task-store-plugin-ase.js +7 -3
  45. package/dst/ase-task-store-plugin-gitea.js +9 -32
  46. package/dst/ase-task-store-plugin-github.js +3 -12
  47. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  48. package/dst/ase-task.js +51 -23
  49. package/dst/ase-util-compat.js +1 -1
  50. package/dst/ase-util-diagram.js +13 -37
  51. package/dst/ase.js +5 -5
  52. package/dst/man/ase-agent-adhoc.md +1165 -0
  53. package/dst/man/ase-agent-condenser.md +383 -0
  54. package/dst/man/ase-agent-editor.md +441 -0
  55. package/dst/man/ase-agent-estimator.md +477 -0
  56. package/dst/man/ase-agent-fleet.md +358 -0
  57. package/dst/man/ase-agent-implementer.md +521 -0
  58. package/dst/man/ase-agent-integrator.md +449 -0
  59. package/dst/man/ase-agent-qualifier.md +426 -0
  60. package/dst/man/ase-agent-rebooter.md +384 -0
  61. package/dst/man/ase-agent-relay.md +262 -0
  62. package/dst/man/ase-agent-researcher.md +478 -0
  63. package/dst/man/ase-agent.md +628 -0
  64. package/dst/man/ase-artifact-list.md +57 -0
  65. package/dst/man/ase-artifact-name.md +56 -0
  66. package/dst/man/ase-artifact.md +92 -0
  67. package/dst/man/ase-config-delete.md +74 -0
  68. package/dst/man/ase-config-edit.md +83 -0
  69. package/dst/man/ase-config-get.md +70 -0
  70. package/dst/man/ase-config-init.md +110 -0
  71. package/dst/man/ase-config-list.md +68 -0
  72. package/dst/man/ase-config-set.md +92 -0
  73. package/dst/man/ase-config.md +426 -0
  74. package/dst/man/ase-hook-permission-request.md +61 -0
  75. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  76. package/dst/man/ase-hook-session-end.md +71 -0
  77. package/dst/man/ase-hook-session-persona.md +65 -0
  78. package/dst/man/ase-hook-session-start.md +184 -0
  79. package/dst/man/ase-hook-stop.md +52 -0
  80. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  81. package/dst/man/ase-hook.md +93 -0
  82. package/dst/man/ase-man.md +195 -0
  83. package/dst/man/ase-mcp.md +102 -0
  84. package/dst/man/ase-service-log.md +59 -0
  85. package/dst/man/ase-service-send.md +60 -0
  86. package/dst/man/ase-service-start.md +93 -0
  87. package/dst/man/ase-service-status.md +58 -0
  88. package/dst/man/ase-service-stop.md +52 -0
  89. package/dst/man/ase-service.md +124 -0
  90. package/dst/man/ase-setup-disable.md +67 -0
  91. package/dst/man/ase-setup-enable.md +66 -0
  92. package/dst/man/ase-setup-install.md +116 -0
  93. package/dst/man/ase-setup-mcp.md +213 -0
  94. package/dst/man/ase-setup-status.md +101 -0
  95. package/dst/man/ase-setup-statusline.md +162 -0
  96. package/dst/man/ase-setup-telemetry.md +166 -0
  97. package/dst/man/ase-setup-uninstall.md +85 -0
  98. package/dst/man/ase-setup-update.md +117 -0
  99. package/dst/man/ase-setup.md +156 -0
  100. package/dst/man/ase-spec-export.md +99 -0
  101. package/dst/man/ase-spec-init.md +57 -0
  102. package/dst/man/ase-spec-lint.md +74 -0
  103. package/dst/man/ase-spec-preview.md +57 -0
  104. package/dst/man/ase-spec.md +85 -0
  105. package/dst/man/ase-statusline.md +330 -0
  106. package/dst/man/ase-task-assign.md +138 -0
  107. package/dst/man/ase-task-board.md +651 -0
  108. package/dst/man/ase-task-delete.md +46 -0
  109. package/dst/man/ase-task-edit.md +73 -0
  110. package/dst/man/ase-task-idscheme.md +92 -0
  111. package/dst/man/ase-task-lifecycle.md +92 -0
  112. package/dst/man/ase-task-list.md +86 -0
  113. package/dst/man/ase-task-load.md +48 -0
  114. package/dst/man/ase-task-newid.md +100 -0
  115. package/dst/man/ase-task-purge.md +49 -0
  116. package/dst/man/ase-task-rename.md +69 -0
  117. package/dst/man/ase-task-request.md +108 -0
  118. package/dst/man/ase-task-save.md +78 -0
  119. package/dst/man/ase-task-status.md +79 -0
  120. package/dst/man/ase-task-store.md +377 -0
  121. package/dst/man/ase-task-view.md +50 -0
  122. package/dst/man/ase-task.md +335 -0
  123. package/dst/man/ase-util-compat.md +38 -0
  124. package/dst/man/ase-util-diagram.md +140 -0
  125. package/dst/man/ase-util-meta.md +68 -0
  126. package/dst/man/ase-util-metric.md +74 -0
  127. package/dst/man/ase-util-mint.md +85 -0
  128. package/dst/man/ase-util-worktree.md +109 -0
  129. package/dst/man/ase-util.md +81 -0
  130. package/dst/man/ase.md +228 -0
  131. package/package.json +12 -17
  132. package/plugin/.claude-plugin/plugin.json +1 -1
  133. package/plugin/.codex-plugin/plugin.json +1 -1
  134. package/plugin/.github/plugin/plugin.json +1 -1
  135. package/plugin/agents/ase-code-lint.md +3 -3
  136. package/plugin/agents/ase-docs-proofread.md +3 -3
  137. package/plugin/agents/ase-docs-refine.md +3 -3
  138. package/plugin/agents/ase-spec-lint.md +338 -0
  139. package/plugin/meta/ase-skill.md +7 -0
  140. package/plugin/mods/ase-mods.ts +10 -7
  141. package/plugin/mods/claude-code.d.ts +26 -3
  142. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  143. package/plugin/skills/ase-code-analyze/help.md +11 -6
  144. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  145. package/plugin/skills/ase-code-lint/help.md +10 -5
  146. package/plugin/skills/ase-help-intent/data.md +204 -13
  147. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  148. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  149. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  150. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  151. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  152. package/plugin/skills/ase-repo-review/help.md +3 -1
  153. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  154. package/plugin/skills/ase-spec-edit/help.md +1 -1
  155. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  156. package/plugin/skills/ase-spec-lint/help.md +171 -0
  157. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  158. package/plugin/skills/ase-task-implement/help.md +11 -1
  159. package/dst/man/ase-agent-adhoc.1 +0 -515
  160. package/dst/man/ase-agent-condenser.1 +0 -193
  161. package/dst/man/ase-agent-editor.1 +0 -212
  162. package/dst/man/ase-agent-estimator.1 +0 -214
  163. package/dst/man/ase-agent-fleet.1 +0 -185
  164. package/dst/man/ase-agent-implementer.1 +0 -233
  165. package/dst/man/ase-agent-integrator.1 +0 -213
  166. package/dst/man/ase-agent-qualifier.1 +0 -214
  167. package/dst/man/ase-agent-rebooter.1 +0 -193
  168. package/dst/man/ase-agent-relay.1 +0 -128
  169. package/dst/man/ase-agent-researcher.1 +0 -217
  170. package/dst/man/ase-agent.1 +0 -264
  171. package/dst/man/ase-artifact-list.1 +0 -38
  172. package/dst/man/ase-artifact-name.1 +0 -39
  173. package/dst/man/ase-artifact.1 +0 -56
  174. package/dst/man/ase-config-delete.1 +0 -45
  175. package/dst/man/ase-config-edit.1 +0 -48
  176. package/dst/man/ase-config-get.1 +0 -46
  177. package/dst/man/ase-config-init.1 +0 -60
  178. package/dst/man/ase-config-list.1 +0 -42
  179. package/dst/man/ase-config-set.1 +0 -52
  180. package/dst/man/ase-config.1 +0 -231
  181. package/dst/man/ase-hook-permission-request.1 +0 -36
  182. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  183. package/dst/man/ase-hook-session-end.1 +0 -47
  184. package/dst/man/ase-hook-session-persona.1 +0 -37
  185. package/dst/man/ase-hook-session-start.1 +0 -81
  186. package/dst/man/ase-hook-stop.1 +0 -34
  187. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  188. package/dst/man/ase-hook.1 +0 -51
  189. package/dst/man/ase-man.1 +0 -67
  190. package/dst/man/ase-mcp.1 +0 -61
  191. package/dst/man/ase-service-log.1 +0 -43
  192. package/dst/man/ase-service-send.1 +0 -47
  193. package/dst/man/ase-service-start.1 +0 -54
  194. package/dst/man/ase-service-status.1 +0 -46
  195. package/dst/man/ase-service-stop.1 +0 -37
  196. package/dst/man/ase-service.1 +0 -85
  197. package/dst/man/ase-setup-disable.1 +0 -44
  198. package/dst/man/ase-setup-enable.1 +0 -45
  199. package/dst/man/ase-setup-install.1 +0 -73
  200. package/dst/man/ase-setup-mcp.1 +0 -124
  201. package/dst/man/ase-setup-status.1 +0 -60
  202. package/dst/man/ase-setup-statusline.1 +0 -92
  203. package/dst/man/ase-setup-telemetry.1 +0 -92
  204. package/dst/man/ase-setup-uninstall.1 +0 -59
  205. package/dst/man/ase-setup-update.1 +0 -64
  206. package/dst/man/ase-setup.1 +0 -93
  207. package/dst/man/ase-spec-export.1 +0 -62
  208. package/dst/man/ase-spec-lint.1 +0 -40
  209. package/dst/man/ase-spec-preview.1 +0 -40
  210. package/dst/man/ase-spec.1 +0 -46
  211. package/dst/man/ase-statusline.1 +0 -189
  212. package/dst/man/ase-task-assign.1 +0 -71
  213. package/dst/man/ase-task-board.1 +0 -384
  214. package/dst/man/ase-task-delete.1 +0 -35
  215. package/dst/man/ase-task-edit.1 +0 -46
  216. package/dst/man/ase-task-idscheme.1 +0 -65
  217. package/dst/man/ase-task-lifecycle.1 +0 -57
  218. package/dst/man/ase-task-list.1 +0 -47
  219. package/dst/man/ase-task-load.1 +0 -36
  220. package/dst/man/ase-task-newid.1 +0 -61
  221. package/dst/man/ase-task-purge.1 +0 -37
  222. package/dst/man/ase-task-rename.1 +0 -44
  223. package/dst/man/ase-task-request.1 +0 -61
  224. package/dst/man/ase-task-save.1 +0 -48
  225. package/dst/man/ase-task-status.1 +0 -55
  226. package/dst/man/ase-task-store.1 +0 -194
  227. package/dst/man/ase-task-view.1 +0 -40
  228. package/dst/man/ase-task.1 +0 -160
  229. package/dst/man/ase-util-compat.1 +0 -25
  230. package/dst/man/ase-util-diagram.1 +0 -76
  231. package/dst/man/ase-util-meta.1 +0 -45
  232. package/dst/man/ase-util-metric.1 +0 -54
  233. package/dst/man/ase-util-mint.1 +0 -59
  234. package/dst/man/ase-util-worktree.1 +0 -72
  235. package/dst/man/ase-util.1 +0 -50
  236. package/dst/man/ase.1 +0 -124
@@ -5,9 +5,122 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
+ import net from "node:net";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { spawn } from "node:child_process";
10
- import { Service } from "./ase-service.js";
11
+ import { Tail } from "tail";
12
+ /* the loopback host to probe the ports on, the range and the number of attempts
13
+ of the random port allocation, and the bounds of the append-only log files:
14
+ maximum tolerated size and number of tail lines surviving a trim */
15
+ const HOST = "127.0.0.1";
16
+ const PORT_MIN = 42000;
17
+ const PORT_MAX = 44000;
18
+ const PORT_TRIES = 20;
19
+ const LOG_MAX_SIZE = 1024 * 1024;
20
+ const LOG_KEEP_LINES = 2000;
21
+ /* reusable functionality: port allocation and log file handling of the background processes */
22
+ export class Daemon {
23
+ /* try binding a single candidate port to verify availability */
24
+ static tryBind(port) {
25
+ return new Promise((resolve) => {
26
+ const s = net.createServer();
27
+ s.once("error", () => {
28
+ resolve(false);
29
+ });
30
+ s.once("listening", () => {
31
+ s.close(() => resolve(true));
32
+ });
33
+ s.listen(port, HOST);
34
+ });
35
+ }
36
+ /* allocate the preferred port, if given and available,
37
+ else a fresh random port in PORT_MIN..PORT_MAX */
38
+ static async allocatePort(pref = null) {
39
+ if (pref !== null && await Daemon.tryBind(pref))
40
+ return pref;
41
+ for (let i = 0; i < PORT_TRIES; i++) {
42
+ const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
43
+ if (await Daemon.tryBind(p))
44
+ return p;
45
+ }
46
+ throw new Error(`failed to allocate a port in ${PORT_MIN}..${PORT_MAX} after ${PORT_TRIES} attempts`);
47
+ }
48
+ /* trim the log file down to its tail if it grew beyond the size limit */
49
+ static trimLog(logFile) {
50
+ try {
51
+ if (!fs.existsSync(logFile) || fs.statSync(logFile).size <= LOG_MAX_SIZE)
52
+ return;
53
+ const tail = Daemon.readLogTail(logFile, LOG_KEEP_LINES);
54
+ fs.writeFileSync(logFile, tail.length > 0 ? `${tail}\n` : "");
55
+ }
56
+ catch {
57
+ /* intentionally ignore all trimming errors, as a
58
+ non-trimmable log file must never block the service */
59
+ }
60
+ }
61
+ /* read the last N non-empty lines of a log file for diagnostics */
62
+ static readLogTail(logFile, lines) {
63
+ let fd = null;
64
+ try {
65
+ fd = fs.openSync(logFile, "r");
66
+ const size = fs.fstatSync(fd).size;
67
+ const CHUNK = 8192;
68
+ const chunks = [];
69
+ let pos = size;
70
+ let count = 0;
71
+ while (pos > 0 && count <= lines) {
72
+ const n = Math.min(CHUNK, pos);
73
+ const buf = Buffer.alloc(n);
74
+ pos -= n;
75
+ fs.readSync(fd, buf, 0, n, pos);
76
+ chunks.unshift(buf);
77
+ for (let i = 0; i < n; i++)
78
+ if (buf[i] === 10)
79
+ count++;
80
+ }
81
+ /* decode once after re-assembly to avoid splitting
82
+ multi-byte UTF-8 sequences at chunk boundaries */
83
+ const all = Buffer.concat(chunks).toString("utf8")
84
+ .split("\n").filter((l) => l.length > 0);
85
+ return all.slice(-lines).join("\n");
86
+ }
87
+ catch {
88
+ return "";
89
+ }
90
+ finally {
91
+ if (fd !== null)
92
+ fs.closeSync(fd);
93
+ }
94
+ }
95
+ /* show a log file on stdout, either completely or its last <lines> lines only, and
96
+ optionally follow it for newly appended lines (default: the last 10 lines first),
97
+ where each line is passed through <transform> (e.g. to colorize parts of it) */
98
+ static async showLog(logFile, opts, transform = (line) => line) {
99
+ const lines = opts.lines !== undefined ? Number(opts.lines) : null;
100
+ if (lines !== null && (!Number.isInteger(lines) || lines < 1))
101
+ throw new Error(`invalid number of lines: ${opts.lines}`);
102
+ if (!fs.existsSync(logFile))
103
+ return 0;
104
+ if (opts.follow) {
105
+ /* follow until interrupted (on log trimming,
106
+ "tail" continues at the new end of the file) */
107
+ const tail = new Tail(logFile, { nLines: lines ?? 10 });
108
+ tail.on("line", (line) => {
109
+ process.stdout.write(`${transform(line)}\n`);
110
+ });
111
+ return await new Promise((_resolve, reject) => {
112
+ tail.on("error", (err) => {
113
+ reject(err instanceof Error ? err : new Error(String(err)));
114
+ });
115
+ });
116
+ }
117
+ const text = lines !== null ?
118
+ Daemon.readLogTail(logFile, lines) :
119
+ fs.readFileSync(logFile, "utf8").replace(/\n$/, "");
120
+ process.stdout.write(text.length > 0 ? `${text.split("\n").map(transform).join("\n")}\n` : "");
121
+ return 0;
122
+ }
123
+ }
11
124
  /* the CLI command group managing a long-running process (a server, an agent, or a fleet
12
125
  of them) in the background or in the foreground: it registers the command group, spawns
13
126
  the process detached into its log file, awaits its startup, checks whether it is alive,
@@ -47,14 +160,14 @@ export class DaemonCommand {
47
160
  "(not yet run in the background, as one run in the foreground logs to the terminal)");
48
161
  return;
49
162
  }
50
- process.exitCode = await Service.showLog(file, opts, transform);
163
+ process.exitCode = await Daemon.showLog(file, opts, transform);
51
164
  });
52
165
  }
53
166
  /* spawn the CLI with the arguments <args> detached, with the additional
54
167
  environment <env>, logging into the (trimmed) log file <logFile> */
55
168
  spawnDetached(args, env, logFile) {
56
169
  fs.mkdirSync(path.dirname(logFile), { recursive: true });
57
- Service.trimLog(logFile);
170
+ Daemon.trimLog(logFile);
58
171
  const fd = fs.openSync(logFile, "a");
59
172
  const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
60
173
  const child = spawn(process.execPath, [entry, ...args], {
@@ -95,7 +208,7 @@ export class DaemonCommand {
95
208
  if (failure === null)
96
209
  return;
97
210
  const reason = `${this.label}: ${failure}`;
98
- const tail = Service.readLogTail(logFile, 20);
211
+ const tail = Daemon.readLogTail(logFile, 20);
99
212
  throw new Error(tail.length > 0 ? `${reason}\n---- ${logFile} (tail) ----\n${tail}` : reason);
100
213
  }
101
214
  /* check whether a process is still alive */
@@ -14,8 +14,7 @@ import { Config } from "./ase-config-core.js";
14
14
  import { parseScope } from "./ase-config-scope.js";
15
15
  import { isLogLevel } from "./ase-lib-log.js";
16
16
  import { writeStdout } from "./ase-lib-stdio.js";
17
- import { Service } from "./ase-service.js";
18
- import { DaemonCommand } from "./ase-lib-daemon-cli.js";
17
+ import { Daemon, DaemonCommand } from "./ase-lib-daemon-cli.js";
19
18
  import { urlHost, isLoopback, serverHeader } from "./ase-lib-server-bind.js";
20
19
  /* the defaults */
21
20
  export const DEFAULT_ADDRESS = "127.0.0.1";
@@ -45,7 +44,7 @@ export const configuredTls = (cfg, prefix) => {
45
44
  if (isLocal(new URL(url.value).hostname.replace(/^\[|\]$/g, "")))
46
45
  return { cert: null, key: null };
47
46
  }
48
- catch (_err) {
47
+ catch {
49
48
  /* an invalid URL is reported by its users */
50
49
  }
51
50
  }
@@ -271,7 +270,7 @@ export class ServerCommand extends DaemonCommand {
271
270
  child.kill("SIGTERM");
272
271
  child.unref();
273
272
  }
274
- const tail = Service.readLogTail(logFile, 20);
273
+ const tail = Daemon.readLogTail(logFile, 20);
275
274
  const reason = exited ?
276
275
  `${this.label}: server exited during startup (code ${exitCode})` :
277
276
  probed === "foreign" ?
@@ -352,7 +351,7 @@ export class ServerCommand extends DaemonCommand {
352
351
  /* re-allocate a configured port held by a foreign service */
353
352
  if (foreign)
354
353
  this.log.write("warning", `${this.label}: configured port ${port} in use by foreign service, re-allocating`);
355
- port = await Service.allocatePort();
354
+ port = await Daemon.allocatePort();
356
355
  }
357
356
  let token = opts.token ?? process.env[this.env("TOKEN")] ?? ctx.token;
358
357
  if (token === undefined || token === null || token === "")
@@ -13,18 +13,27 @@ export default class Version {
13
13
  return pkg.version;
14
14
  }
15
15
  /* return latest ASE version known from the NPM registry
16
- (cached background check, falling back to the current version) */
17
- static async latest() {
16
+ (cached background check or, if requested, immediate direct check,
17
+ both falling back to the current version) */
18
+ static async latest(direct = false) {
18
19
  const notifier = updateNotifier({
19
20
  pkg,
20
21
  updateCheckInterval: 1000 * 60 * 60
21
22
  });
22
- /* re-persist the cached update information, as update-notifier consumes it on read,
23
+ /* optionally query the NPM registry directly, where only
24
+ a successful query supersedes the cached update information */
25
+ let update = notifier.update;
26
+ if (direct) {
27
+ const info = await Promise.resolve(notifier.fetchInfo()).catch(() => undefined);
28
+ if (info !== undefined)
29
+ update = info;
30
+ }
31
+ /* re-persist the update information, as update-notifier consumes it on read,
23
32
  but only while it still denotes a newer version (it is never cleared otherwise) */
24
- const latest = notifier.update?.latest;
33
+ const latest = update?.latest;
25
34
  if (latest === undefined || !semver.valid(latest) || !semver.gt(latest, Version.current()))
26
35
  return Version.current();
27
- notifier.config?.set("update", notifier.update);
36
+ notifier.config?.set("update", update);
28
37
  return latest;
29
38
  }
30
39
  }
@@ -0,0 +1,229 @@
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 { fuzzyMin, levenshtein, diceCoefficient } from "./ase-task-board-filter.js";
10
+ /* the grouping and ordering of the skills: a copy of the groups shown by the
11
+ catalog of the website ("skillPrefixGroups" of "pages/src/data/skills.ts",
12
+ in the column order of "Section-Usage.astro"), so keep both in sync, where
13
+ any skill not listed here is caught by a trailing "OTHER" group */
14
+ const skillGroups = [
15
+ ["HELP", ["ase-help-skill", "ase-help-intent"]],
16
+ ["META: CONFIG", ["ase-meta-config", "ase-meta-compat"]],
17
+ ["META: EXPLAIN", ["ase-meta-why", "ase-meta-eli5"]],
18
+ ["META: JUDGE", ["ase-meta-evaluate", "ase-meta-diaboli", "ase-meta-steelman"]],
19
+ ["META: RESEARCH", ["ase-meta-brainstorm", "ase-meta-search", "ase-meta-quorum",
20
+ "ase-meta-proximity", "ase-meta-chat", "ase-meta-quotes"]],
21
+ ["META: GENERATE", ["ase-meta-mint", "ase-meta-workflow"]],
22
+ ["ARCH", ["ase-arch-analyze", "ase-arch-discover"]],
23
+ ["DOCS", ["ase-docs-shorten", "ase-docs-refine", "ase-docs-proofread", "ase-docs-distill"]],
24
+ ["SPEC", ["ase-spec-lint", "ase-spec-edit", "ase-spec-activate"]],
25
+ ["CODE", ["ase-code-edit", "ase-code-analyze", "ase-code-insight", "ase-code-explain", "ase-code-lint"]],
26
+ ["REPO", ["ase-repo-review", "ase-repo-diff", "ase-repo-changelog", "ase-repo-commit",
27
+ "ase-repo-dissect", "ase-repo-merge", "ase-repo-resolve"]],
28
+ ["SYNC", ["ase-sync-reconcile", "ase-sync-import", "ase-sync-export"]],
29
+ ["TASK: MANAGE", ["ase-task-list", "ase-task-rename", "ase-task-delete"]],
30
+ ["TASK: HANDLE", ["ase-task-id", "ase-task-status", "ase-task-assign"]],
31
+ ["TASK: EDIT", ["ase-task-edit", "ase-task-view", "ase-task-grill", "ase-task-research",
32
+ "ase-task-estimate", "ase-task-reboot", "ase-task-condense", "ase-task-dissect"]],
33
+ ["TASK: EXECUTE", ["ase-task-implement", "ase-task-integrate"]]
34
+ ];
35
+ /* the order of the command groups: a copy of the column order of "manColumns"
36
+ of "pages/src/data/manpages.ts", so keep both in sync, where any group not
37
+ listed here (like the one of a newly added top-level command) is appended */
38
+ const commandGroups = ["Top-Level", "setup", "config", "service", "task", "spec", "util", "artifact", "agent", "hook"];
39
+ /* read the Markdown sources of the entries of a directory, by the page
40
+ names and the relative source paths derived from the entry names */
41
+ const readSources = (dir, pick) => {
42
+ let entries;
43
+ try {
44
+ entries = fs.readdirSync(dir);
45
+ }
46
+ catch (err) {
47
+ throw new Error(`man: no manual pages found in "${dir}" (tool not completely built?)`, { cause: err });
48
+ }
49
+ const sources = new Map();
50
+ for (const entry of entries) {
51
+ const picked = pick(entry);
52
+ if (picked !== null && fs.existsSync(path.join(dir, picked[1])))
53
+ sources.set(picked[0], fs.readFileSync(path.join(dir, picked[1]), "utf8"));
54
+ }
55
+ return sources;
56
+ };
57
+ /* create a manual page */
58
+ const makePage = (id, kind, label, title, md) => {
59
+ const text = md.toLowerCase();
60
+ return { id, kind, label, title, md, text, words: new Set(text.split(/[^a-z0-9]+/).filter((w) => w !== "")) };
61
+ };
62
+ /* load the skill pages from the "help.md" files of the bundled plugin (see
63
+ the "build-plugin" build step), grouped like the catalog of the website */
64
+ const loadSkills = () => {
65
+ const pkgdir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
66
+ const dir = path.join(pkgdir, "plugin", "skills");
67
+ const sources = readSources(dir, (entry) => /^ase-[a-z0-9-]+$/.test(entry) ? [entry, path.join(entry, "help.md")] : null);
68
+ const page = (name) => {
69
+ const md = sources.get(name);
70
+ const m = /^#+\s+NAME\s*\n+\s*`?[\w-]+`?\s+-+\s+(.+)$/m.exec(md);
71
+ return makePage(name, "skill", name, m !== null ? m[1].trim() : name, md);
72
+ };
73
+ const listed = new Set(skillGroups.flatMap(([, names]) => names));
74
+ const other = [...sources.keys()].filter((name) => !listed.has(name)).sort();
75
+ return [...skillGroups, ["OTHER", other]]
76
+ .map(([label, names]) => ({ label, pages: names.filter((name) => sources.has(name)).map(page) }))
77
+ .filter((group) => group.pages.length > 0);
78
+ };
79
+ /* load the command pages from the Markdown sources of the manual pages
80
+ (see the "build-man" build step), arranged into the command tree and
81
+ grouped like the catalog of the website ("pages/src/data/manpages.ts") */
82
+ const loadCommands = () => {
83
+ const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), "man");
84
+ const sources = readSources(dir, (entry) => /^ase(?:-[a-z0-9-]+)?\.md$/.test(entry) ? [entry.slice(0, -3), entry] : null);
85
+ const names = [...sources.keys()].sort();
86
+ /* the parent of a page is the longest other page name it extends by a "-<suffix>" */
87
+ const parentOf = (name) => names.filter((other) => name.startsWith(other + "-"))
88
+ .sort((a, b) => b.length - a.length)[0] ?? "";
89
+ /* the children of a page, ordered by their first "`<name>`(1)" reference
90
+ within the COMMANDS section of the page (or the entire page), else alphabetically */
91
+ const childrenOf = (parent) => {
92
+ const md = sources.get(parent);
93
+ const m = /^## COMMANDS\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(md);
94
+ const section = m !== null ? m[1] : md;
95
+ const pos = (name) => {
96
+ const idx = section.indexOf("`" + name + "`(1)");
97
+ return idx >= 0 ? idx : Infinity;
98
+ };
99
+ return names.filter((name) => parentOf(name) === parent)
100
+ .sort((a, b) => (pos(a) - pos(b)) || a.localeCompare(b));
101
+ };
102
+ /* flatten the command tree depth-first */
103
+ const tree = [];
104
+ const walk = (name, label, depth) => {
105
+ const md = sources.get(name);
106
+ const m = /^#\s+\S+\(1\)\s+--\s+(.+)$/m.exec(md);
107
+ tree.push({ page: makePage(`${name}.1`, "command", label, m !== null ? m[1].trim() : name, md), depth });
108
+ for (const child of childrenOf(name))
109
+ walk(child, label + " " + child.slice(name.length + 1), depth + 1);
110
+ };
111
+ for (const root of names.filter((name) => parentOf(name) === ""))
112
+ walk(root, root.replace(/-/g, " "), 0);
113
+ /* group the command tree: "ase" itself and the top-level commands without
114
+ sub-commands form the "Top-Level" group, and every other top-level
115
+ command forms an own group with all of its deeper commands */
116
+ const groups = [{ label: "Top-Level", pages: [] }];
117
+ for (const { page, depth } of tree) {
118
+ if (depth === 0)
119
+ groups[0].pages.push(page);
120
+ else if (depth === 1) {
121
+ if (tree.some((other) => other.depth > 1 && other.page.label.startsWith(page.label + " ")))
122
+ groups.push({ label: page.label.slice(page.label.lastIndexOf(" ") + 1), pages: [page] });
123
+ else
124
+ groups[0].pages.push(page);
125
+ }
126
+ else
127
+ groups[groups.length - 1].pages.push(page);
128
+ }
129
+ const rank = (group) => {
130
+ const idx = commandGroups.indexOf(group.label);
131
+ return idx >= 0 ? idx : commandGroups.length;
132
+ };
133
+ return groups
134
+ .filter((group) => group.pages.length > 0)
135
+ .sort((a, b) => rank(a) - rank(b))
136
+ .map((group) => ({ ...group, label: group.label.toUpperCase() }));
137
+ };
138
+ /* load the catalog of all manual pages */
139
+ export const loadCatalog = () => {
140
+ const skills = loadSkills();
141
+ const commands = loadCommands();
142
+ const pages = new Map([...skills, ...commands].flatMap((group) => group.pages).map((page) => [page.id, page]));
143
+ const vocab = new Set([...pages.values()].flatMap((page) => [...page.words]));
144
+ return { skills, commands, pages, vocab };
145
+ };
146
+ /* find the page fitting exactly onto the words of a page name or command path,
147
+ which are joined with dashes, where the "ase" prefix is optional, the name is
148
+ case-insensitive, and of a skill and a command of the same name, a page name
149
+ (e.g. "task-edit") prefers the skill, while a command path (e.g. "task edit")
150
+ prefers the command */
151
+ export const findPage = (catalog, words) => {
152
+ const parts = words.join(" ").trim().toLowerCase().split(/\s+/);
153
+ const name = parts.join("-");
154
+ const full = name === "ase" || name.startsWith("ase-") ? name : `ase-${name}`;
155
+ const skill = catalog.pages.get(full);
156
+ const command = catalog.pages.get(`${full}.1`);
157
+ return parts.length === 1 && name.includes("-") ? skill ?? command : command ?? skill;
158
+ };
159
+ /* escape a string for its literal use within a regular expression */
160
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ /* the regular expression of the matches of a search in a page: the
162
+ keywords as plain substrings, plus the fuzzy variants of the page as
163
+ whole words, with the longest alternative first, so a variant is never
164
+ cut short by a keyword nested inside it */
165
+ const matchRE = (keywords, variants) => {
166
+ const alts = keywords.map((k) => ({ len: k.length, src: escape(k) }))
167
+ .concat(variants.map((w) => ({ len: w.length, src: "(?<![a-z0-9])" + escape(w) + "(?![a-z0-9])" })));
168
+ alts.sort((a, b) => b.len - a.len);
169
+ return new RegExp(alts.map((a) => a.src).join("|"), "gi");
170
+ };
171
+ /* search all pages for a query (as the website does): all whitespace-separated
172
+ keywords have to occur in a page (AND logic), in up to two passes, the
173
+ keywords as literal substrings first and, only if this matches no page at
174
+ all, as fuzzy variants of their words, as the edit-distance neighborhood of
175
+ a keyword would otherwise dilute a correctly spelled query; the result
176
+ maps the ids of the matching pages onto the regular expressions of their
177
+ matches, or is null for a query without any keywords */
178
+ export const searchPages = (catalog, query) => {
179
+ const keywords = query.toLowerCase().split(/\s+/).filter((word) => word !== "");
180
+ if (keywords.length === 0)
181
+ return null;
182
+ const pages = [...catalog.pages.values()];
183
+ const hits = new Map();
184
+ for (const page of pages)
185
+ if (keywords.every((k) => page.text.includes(k)))
186
+ hits.set(page.id, matchRE(keywords, []));
187
+ if (hits.size > 0)
188
+ return hits;
189
+ /* the fuzzy-equivalent vocabulary words of a keyword, cached per keyword */
190
+ const fuzzy = new Map();
191
+ const fuzzyWords = (word) => {
192
+ let result = fuzzy.get(word);
193
+ if (result === undefined) {
194
+ result = word.length < fuzzyMin ? [] : [...catalog.vocab].filter((given) => Math.abs(word.length - given.length) <= 1
195
+ && (diceCoefficient(word, given) >= 0.50 || levenshtein(word, given) <= 2));
196
+ fuzzy.set(word, result);
197
+ }
198
+ return result;
199
+ };
200
+ /* the regular expression of the fuzzy variants of a compound keyword (like
201
+ "ase-task-edit"), with each of its words literally or fuzzily replaced
202
+ and its separators kept, cached per keyword (null for a single word) */
203
+ const compound = new Map();
204
+ const compoundRE = (keyword) => {
205
+ let result = compound.get(keyword);
206
+ if (result === undefined) {
207
+ const parts = keyword.split(/([^a-z0-9]+)/);
208
+ result = parts.length < 2 ? null : new RegExp("(?<![a-z0-9])" + parts.map((part, i) => i % 2 === 1 ? escape(part) : part === "" ? "" :
209
+ "(?:" + [part, ...fuzzyWords(part)].map(escape).join("|") + ")").join("") + "(?![a-z0-9])", "g");
210
+ compound.set(keyword, result);
211
+ }
212
+ return result;
213
+ };
214
+ for (const page of pages) {
215
+ const variants = [];
216
+ const hit = keywords.every((k) => {
217
+ if (page.text.includes(k))
218
+ return true;
219
+ const re = compoundRE(k);
220
+ const near = re !== null ? [...new Set([...page.text.matchAll(re)].map((m) => m[0]))] :
221
+ fuzzyWords(k).filter((w) => page.words.has(w));
222
+ variants.push(...near);
223
+ return near.length > 0;
224
+ });
225
+ if (hit)
226
+ hits.set(page.id, matchRE(keywords, variants));
227
+ }
228
+ return hits;
229
+ };