@rse/ase 1.0.7 → 2.0.1

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 +357 -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} +8 -1
  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 +269 -81
  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 +231 -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 +81 -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
@@ -13,24 +13,30 @@ import { ofetch } from "ofetch";
13
13
  import { isMap } from "yaml";
14
14
  import prettyMs from "pretty-ms";
15
15
  import * as v from "valibot";
16
+ import { Tail } from "tail";
16
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
18
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
- import { Config, configSchema, ConfigMCP } from "./ase-config.js";
19
- import { isLogLevel } from "./ase-log.js";
20
- import { CompatMCP } from "./ase-compat.js";
21
- import { DiagramMCP } from "./ase-diagram.js";
22
- import { TaskMCP } from "./ase-task.js";
23
- import { MarkdownMCP } from "./ase-markdown.js";
19
+ import { ConfigMCP } from "./ase-config.js";
20
+ import { Config } from "./ase-config-core.js";
21
+ import { configSchema } from "./ase-config-schema.js";
22
+ import { ensureAseGitignore } from "./ase-config-scope.js";
23
+ import { isLogLevel } from "./ase-lib-log.js";
24
+ import { CompatMCP } from "./ase-util-compat.js";
25
+ import { DiagramMCP } from "./ase-util-diagram.js";
26
+ import { Task, TaskMCP } from "./ase-task.js";
27
+ import { MarkdownMCP } from "./ase-service-markdown.js";
24
28
  import { ArtifactMCP } from "./ase-artifact.js";
25
29
  import { SpecMCP } from "./ase-spec.js";
26
- import { KVMCP } from "./ase-kv.js";
27
- import { TimestampMCP } from "./ase-timestamp.js";
28
- import { SleepMCP } from "./ase-sleep.js";
29
- import { GetoptMCP } from "./ase-getopt.js";
30
- import { SkillsMCP } from "./ase-skills.js";
31
- import { WorktreeMCP } from "./ase-worktree.js";
32
- import { MintMCP } from "./ase-mint.js";
33
- import { MetricMCP } from "./ase-metric.js";
30
+ import { KVMCP } from "./ase-service-kv.js";
31
+ import { StateMCP } from "./ase-service-state.js";
32
+ import { TimestampMCP } from "./ase-service-timestamp.js";
33
+ import { SleepMCP } from "./ase-service-sleep.js";
34
+ import { GetoptMCP } from "./ase-service-getopt.js";
35
+ import { SkillsMCP } from "./ase-service-skills.js";
36
+ import { WorktreeMCP } from "./ase-util-worktree.js";
37
+ import { MintMCP } from "./ase-util-mint.js";
38
+ import { MetricMCP } from "./ase-util-metric.js";
39
+ import { UsageTracker } from "./ase-service-otel.js";
34
40
  import pkg from "../package.json" with { type: "json" };
35
41
  /* shared service host */
36
42
  export const SERVICE_HOST = "127.0.0.1";
@@ -80,7 +86,7 @@ const LOG_MAX_SIZE = 1024 * 1024;
80
86
  const LOG_KEEP_LINES = 2000;
81
87
  const LOG_ARGS_MAX = 200;
82
88
  /* load the optional "config.yaml" and "service.yaml" files and derive
83
- the service identity context (project id, port, service config) */
89
+ the service identity context (project id, port, preferred port, service config) */
84
90
  export const loadServiceContext = (log) => {
85
91
  /* load files */
86
92
  const cfg = new Config("config", configSchema, log);
@@ -88,13 +94,16 @@ export const loadServiceContext = (log) => {
88
94
  const svc = new Config("service", serviceSchema, log);
89
95
  svc.read();
90
96
  /* determine project id */
91
- const projectId = cfg.get("project.id") ?? path.basename(process.cwd());
97
+ const projectId = cfg.get("project.id") || Task.projectIdOf(Task.projectRoot());
92
98
  /* determine service port */
93
99
  const port = svc.get("port") ?? null;
100
+ /* determine preferred service port */
101
+ const pref = cfg.get("project.service.port");
102
+ const portPref = pref !== null && pref !== undefined ? Number(pref) : null;
94
103
  /* determine path to ".ase" directory */
95
104
  const aseDir = path.dirname(svc.filename);
96
105
  /* return context information */
97
- return { projectId, port, svc, aseDir };
106
+ return { projectId, port, portPref, svc, aseDir };
98
107
  };
99
108
  /* reusable functionality: port allocation, persistence, and process spawning */
100
109
  export class Service {
@@ -111,8 +120,11 @@ export class Service {
111
120
  s.listen(port, HOST);
112
121
  });
113
122
  }
114
- /* allocate a fresh random port in PORT_MIN..PORT_MAX */
115
- static async allocatePort() {
123
+ /* allocate the preferred port, if given and available,
124
+ else a fresh random port in PORT_MIN..PORT_MAX */
125
+ static async allocatePort(pref = null) {
126
+ if (pref !== null && await Service.tryBind(pref))
127
+ return pref;
116
128
  for (let i = 0; i < PORT_TRIES; i++) {
117
129
  const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
118
130
  if (await Service.tryBind(p))
@@ -122,6 +134,7 @@ export class Service {
122
134
  }
123
135
  /* persist an allocated port into ".ase/service.yaml" */
124
136
  static persistPort(svc, port) {
137
+ ensureAseGitignore(path.dirname(svc.filename));
125
138
  svc.lock(() => {
126
139
  svc.read();
127
140
  svc.set("port", port);
@@ -159,6 +172,7 @@ export class Service {
159
172
  /* spawn the current executable detached as a background service */
160
173
  static spawnDetached(aseDir, port, logLevel) {
161
174
  fs.mkdirSync(aseDir, { recursive: true });
175
+ ensureAseGitignore(aseDir);
162
176
  const logFile = path.join(aseDir, "service.log");
163
177
  /* trim the log before handing it to the service, as the detached
164
178
  service inherits the file descriptor for its entire lifetime and
@@ -208,6 +222,34 @@ export class Service {
208
222
  fs.closeSync(fd);
209
223
  }
210
224
  }
225
+ /* show a log file on stdout, either completely or its last <lines> lines only, and
226
+ optionally follow it for newly appended lines (default: the last 10 lines first),
227
+ where each line is passed through <transform> (e.g. to colorize parts of it) */
228
+ static async showLog(logFile, opts, transform = (line) => line) {
229
+ const lines = opts.lines !== undefined ? Number(opts.lines) : null;
230
+ if (lines !== null && (!Number.isInteger(lines) || lines < 1))
231
+ throw new Error(`invalid number of lines: ${opts.lines}`);
232
+ if (!fs.existsSync(logFile))
233
+ return 0;
234
+ if (opts.follow) {
235
+ /* follow until interrupted (on log trimming,
236
+ "tail" continues at the new end of the file) */
237
+ const tail = new Tail(logFile, { nLines: lines ?? 10 });
238
+ tail.on("line", (line) => {
239
+ process.stdout.write(`${transform(line)}\n`);
240
+ });
241
+ return await new Promise((_resolve, reject) => {
242
+ tail.on("error", (err) => {
243
+ reject(err instanceof Error ? err : new Error(String(err)));
244
+ });
245
+ });
246
+ }
247
+ const text = lines !== null ?
248
+ Service.readLogTail(logFile, lines) :
249
+ fs.readFileSync(logFile, "utf8").replace(/\n$/, "");
250
+ process.stdout.write(text.length > 0 ? `${text.split("\n").map(transform).join("\n")}\n` : "");
251
+ return 0;
252
+ }
211
253
  }
212
254
  /* MCP registration entry point for service-identity tools */
213
255
  export class ServiceMCP {
@@ -243,8 +285,56 @@ export default class ServiceCommand {
243
285
  loadContext() {
244
286
  return loadServiceContext(this.log);
245
287
  }
246
- /* service-side: bind HAPI server until "/stop" command is received or idle timeout happens */
247
- async runService(ctx) {
288
+ /* build a fresh MCP server instance with all registered tools
289
+ (also used by the "ase mcp" bridge for answering the MCP handshake
290
+ and tool listing in-process before the service is started) */
291
+ static buildMcpServer(log, ctx, startTime, usage) {
292
+ const mcp = new McpServer({ name: "ase", version: pkg.version });
293
+ new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
294
+ new CompatMCP().register(mcp);
295
+ new DiagramMCP().register(mcp);
296
+ new TaskMCP(log, usage).register(mcp);
297
+ new MarkdownMCP().register(mcp);
298
+ new ArtifactMCP(log).register(mcp);
299
+ new SpecMCP(log).register(mcp);
300
+ new KVMCP().register(mcp);
301
+ new TimestampMCP().register(mcp);
302
+ new SleepMCP().register(mcp);
303
+ new GetoptMCP().register(mcp);
304
+ new SkillsMCP().register(mcp);
305
+ new WorktreeMCP().register(mcp);
306
+ new MintMCP().register(mcp);
307
+ new MetricMCP().register(mcp);
308
+ new ConfigMCP(log).register(mcp);
309
+ new StateMCP(log).register(mcp);
310
+ return mcp;
311
+ }
312
+ /* summarize the method, tool name, and capped tool arguments of an MCP request body */
313
+ static mcpBodyInfo(body) {
314
+ const b = body;
315
+ const bParams = b?.params;
316
+ const bMethod = typeof b?.method === "string" ? b.method : null;
317
+ const bName = typeof bParams?.name === "string" ? bParams.name : null;
318
+ const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
319
+ let info = "";
320
+ if (bMethod !== null) {
321
+ info = ` [${bMethod}]`;
322
+ if (bName !== null) {
323
+ info += ` ${bName}`;
324
+ if (bArgs !== null) {
325
+ /* cap the arguments, as payload-carrying tool calls
326
+ (task plans, key/value batches, etc) would
327
+ otherwise dominate the entire log file */
328
+ const args = JSON.stringify(bArgs);
329
+ info += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
330
+ }
331
+ }
332
+ }
333
+ return { method: bMethod, info };
334
+ }
335
+ /* service-side: bind HAPI server until "/stop" command is received or idle timeout happens
336
+ (or, in the foreground, until "/stop" command is received or a termination signal arrives) */
337
+ async runService(ctx, foreground = false) {
248
338
  /* establish HAPI HTTP/REST service */
249
339
  const server = Hapi.server({ host: HOST, port: ctx.port });
250
340
  /* track start time and last activity */
@@ -252,9 +342,14 @@ export default class ServiceCommand {
252
342
  let lastActivity = Date.now();
253
343
  let inFlight = 0;
254
344
  let stopping = false;
255
- server.ext("onRequest", (_request, h) => {
345
+ /* accept only loopback "Host" headers, to defeat DNS rebinding attacks from browser pages */
346
+ const allowedHosts = new Set([`${HOST}:${ctx.port}`, `localhost:${ctx.port}`]);
347
+ server.ext("onRequest", (request, h) => {
256
348
  inFlight++;
257
349
  lastActivity = Date.now();
350
+ const host = (request.headers.host ?? "").toLowerCase();
351
+ if (!allowedHosts.has(host))
352
+ return h.response({ error: "invalid Host header" }).code(421).takeover();
258
353
  return h.continue;
259
354
  });
260
355
  server.ext("onPreResponse", (_request, h) => {
@@ -262,27 +357,10 @@ export default class ServiceCommand {
262
357
  lastActivity = Date.now();
263
358
  return h.continue;
264
359
  });
265
- /* build a fresh MCP server instance with all registered tools */
266
- const buildMcpServer = () => {
267
- const mcp = new McpServer({ name: "ase", version: pkg.version });
268
- new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
269
- new CompatMCP().register(mcp);
270
- new DiagramMCP().register(mcp);
271
- new TaskMCP(this.log).register(mcp);
272
- new MarkdownMCP().register(mcp);
273
- new ArtifactMCP(this.log).register(mcp);
274
- new SpecMCP(this.log).register(mcp);
275
- new KVMCP().register(mcp);
276
- new TimestampMCP().register(mcp);
277
- new SleepMCP().register(mcp);
278
- new GetoptMCP().register(mcp);
279
- new SkillsMCP().register(mcp);
280
- new WorktreeMCP().register(mcp);
281
- new MintMCP().register(mcp);
282
- new MetricMCP().register(mcp);
283
- new ConfigMCP(this.log).register(mcp);
284
- return mcp;
285
- };
360
+ /* establish the usage tracker of the AI usage accounting (and the OTLP ingress) */
361
+ const usage = new UsageTracker(this.log, Task.projectRoot(), ctx.projectId, ctx.aseDir, () => {
362
+ lastActivity = Date.now();
363
+ });
286
364
  /* listen to HTTP/REST endpoints */
287
365
  server.route({
288
366
  method: "OPTIONS",
@@ -292,9 +370,14 @@ export default class ServiceCommand {
292
370
  }
293
371
  });
294
372
  server.route({
295
- method: "GET",
373
+ method: "POST",
296
374
  path: "/stop",
297
- handler: (_request, h) => {
375
+ handler: (request, h) => {
376
+ /* require a JSON content type, as browsers cannot send it cross-origin
377
+ without a CORS preflight (which this service never grants) */
378
+ const ct = (request.headers["content-type"] ?? "").toLowerCase();
379
+ if (!ct.startsWith("application/json"))
380
+ return h.response({ error: "unsupported media type" }).code(415);
298
381
  this.log.write("info", "service: stop requested");
299
382
  setImmediate(async () => {
300
383
  try {
@@ -304,38 +387,21 @@ export default class ServiceCommand {
304
387
  const e = err;
305
388
  this.log.write("error", `service: stop failed: ${e.message}`);
306
389
  }
390
+ await usage.stop();
307
391
  process.exit(0);
308
392
  });
309
393
  return h.response({ ok: true }).code(200);
310
394
  }
311
395
  });
312
396
  const mcpHandler = async (request, h, body) => {
313
- const b = body;
314
- const bParams = b?.params;
315
- const bMethod = typeof b?.method === "string" ? b.method : null;
316
- const bName = typeof bParams?.name === "string" ? bParams.name : null;
317
- const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
318
- let bodyInfo = "";
319
- if (bMethod !== null) {
320
- bodyInfo = ` [${bMethod}]`;
321
- if (bName !== null) {
322
- bodyInfo += ` ${bName}`;
323
- if (bArgs !== null) {
324
- /* cap the arguments, as payload-carrying tool calls
325
- (task plans, key/value batches, etc) would
326
- otherwise dominate the entire log file */
327
- const args = JSON.stringify(bArgs);
328
- bodyInfo += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
329
- }
330
- }
331
- }
397
+ const { method: bMethod, info: bodyInfo } = ServiceCommand.mcpBodyInfo(body);
332
398
  /* log tool calls regularly, but all remaining MCP traffic
333
399
  (session handshakes, notifications, SSE stream opens) at
334
400
  debug level only, as it carries no diagnostic value */
335
401
  const level = bMethod === "tools/call" ? "info" : "debug";
336
402
  this.log.write(level, `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
337
403
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
338
- const mcp = buildMcpServer();
404
+ const mcp = ServiceCommand.buildMcpServer(this.log, ctx, startTime, usage);
339
405
  request.raw.res.on("close", () => {
340
406
  /* "h.abandon" (see below) bypasses "onPreResponse",
341
407
  so undo the "onRequest" accounting here instead */
@@ -390,11 +456,77 @@ export default class ServiceCommand {
390
456
  return h.response({ error: "unknown command", command: cmd }).code(400);
391
457
  }
392
458
  });
459
+ /* listen to the endpoints of the AI usage accounting: the usage records forwarded
460
+ by the OTLP ingress, the session end, the task assignment on behalf of a session,
461
+ and the task rename */
462
+ server.route({
463
+ method: "POST",
464
+ path: "/usage/records",
465
+ options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
466
+ handler: (request, h) => {
467
+ const payload = request.payload;
468
+ if (!Array.isArray(payload?.records))
469
+ return h.response({ error: "missing or invalid 'records' field" }).code(400);
470
+ usage.ingest(payload.records);
471
+ return h.response({ ok: true }).code(200);
472
+ }
473
+ });
474
+ server.route({
475
+ method: "POST",
476
+ path: "/usage/detach",
477
+ options: { payload: { parse: true, allow: "application/json" } },
478
+ handler: (request, h) => {
479
+ const payload = request.payload;
480
+ if (typeof payload?.session !== "string")
481
+ return h.response({ error: "missing or invalid 'session' field" }).code(400);
482
+ usage.detach(payload.session);
483
+ return h.response({ ok: true }).code(200);
484
+ }
485
+ });
486
+ server.route({
487
+ method: "POST",
488
+ path: "/usage/rename",
489
+ options: { payload: { parse: true, allow: "application/json" } },
490
+ handler: (request, h) => {
491
+ const payload = request.payload;
492
+ if (typeof payload?.old !== "string" || typeof payload.new !== "string")
493
+ return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
494
+ try {
495
+ usage.rename(payload.old, payload.new);
496
+ }
497
+ catch (err) {
498
+ return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
499
+ }
500
+ return h.response({ ok: true }).code(200);
501
+ }
502
+ });
503
+ server.route({
504
+ method: "POST",
505
+ path: "/usage/assign",
506
+ options: { payload: { parse: true, allow: "application/json" } },
507
+ handler: async (request, h) => {
508
+ const payload = request.payload;
509
+ if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
510
+ return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
511
+ try {
512
+ return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
513
+ }
514
+ catch (err) {
515
+ return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
516
+ }
517
+ }
518
+ });
519
+ /* serve the web board of the project (imported dynamically to keep it out of all
520
+ other CLI commands; its rendering dependencies marked, MRCS, and ELK load on
521
+ first request only, its board core dependencies at service startup) */
522
+ const { registerBoardRoutes } = await import("./ase-task-board-web-server.js");
523
+ registerBoardRoutes(server, this.log);
393
524
  /* start service */
394
525
  try {
395
526
  await server.start();
396
527
  Service.persistPort(ctx.svc, ctx.port);
397
528
  this.log.write("info", `service: listening on port ${ctx.port}`);
529
+ usage.start();
398
530
  }
399
531
  catch (err) {
400
532
  const e = err;
@@ -410,31 +542,67 @@ export default class ServiceCommand {
410
542
  this.log.write("error", `service: ${e.message}`);
411
543
  process.exit(1);
412
544
  }
545
+ /* stop service in the foreground on termination signal */
546
+ if (foreground) {
547
+ const shutdown = async (signal) => {
548
+ if (stopping)
549
+ return;
550
+ stopping = true;
551
+ this.log.write("info", `service: received ${signal}, stopping`);
552
+ try {
553
+ await server.stop({ timeout: 1000 });
554
+ }
555
+ catch (err) {
556
+ const e = err;
557
+ this.log.write("error", `service: stop failed: ${e.message}`);
558
+ }
559
+ await usage.stop();
560
+ Service.clearPort(ctx.svc);
561
+ process.exit(0);
562
+ };
563
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
564
+ process.on("SIGINT", () => shutdown("SIGINT"));
565
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
566
+ process.on("SIGQUIT", () => shutdown("SIGQUIT"));
567
+ return;
568
+ }
413
569
  /* stop service after idle timeout */
414
570
  setInterval(async () => {
415
571
  if (stopping)
416
572
  return;
417
573
  if (inFlight > 0)
418
574
  return;
575
+ /* never idle out while holding usage windows, as their sessions keep on reporting */
576
+ if (usage.active())
577
+ return;
419
578
  if (Date.now() - lastActivity > IDLE_MS) {
420
579
  stopping = true;
421
580
  this.log.write("info", "service: idle timeout reached, stopping");
422
581
  try {
423
582
  await server.stop({ timeout: 1000 });
583
+ await usage.stop();
424
584
  Service.clearPort(ctx.svc);
425
585
  process.exit(0);
426
586
  }
427
587
  catch (err) {
428
588
  const e = err;
429
589
  this.log.write("error", `service: stop failed: ${e.message}`);
590
+ await usage.stop();
430
591
  Service.clearPort(ctx.svc);
431
592
  process.exit(1);
432
593
  }
433
594
  }
434
595
  }, TICK_MS).unref();
435
596
  }
436
- /* start flow: ensure port, probe, optionally detach */
437
- async doStart() {
597
+ /* allocate the service port, preferring the configured one, else falling back to a random one */
598
+ async allocatePort(ctx) {
599
+ const port = await Service.allocatePort(ctx.portPref);
600
+ if (ctx.portPref !== null && port !== ctx.portPref)
601
+ this.log.write("warning", `service: preferred port ${ctx.portPref} unavailable, falling back to port ${port}`);
602
+ return port;
603
+ }
604
+ /* start flow: ensure port, probe, optionally detach (or run in the foreground) */
605
+ async doStart(opts = {}) {
438
606
  const ctx = this.loadContext();
439
607
  let port = ctx.port;
440
608
  if (process.env[SERVE_ENV] === "1") {
@@ -451,15 +619,23 @@ export default class ServiceCommand {
451
619
  if (port !== null) {
452
620
  const match = await probe(port, ctx.projectId).catch(() => null);
453
621
  if (match === true) {
622
+ if (opts.foreground)
623
+ throw new Error(`service: already running on port ${port} (stop it first with "ase service stop")`);
454
624
  this.log.write("info", `service: already running on port ${port}`);
455
625
  return 0;
456
626
  }
457
627
  }
628
+ /* run the service in the foreground, logging to the terminal and without idle timeout */
629
+ if (opts.foreground) {
630
+ port = await this.allocatePort(ctx);
631
+ await this.runService({ ...ctx, port }, true);
632
+ return await new Promise(() => { });
633
+ }
458
634
  /* bounded retry across the bind/start TOCTOU window: on each attempt
459
635
  re-allocate, re-spawn; early-break on foreign listener */
460
636
  let lastErr = new Error("service failed to start within timeout");
461
637
  for (let attempt = 0; attempt < 3; attempt++) {
462
- port = await Service.allocatePort();
638
+ port = await this.allocatePort(ctx);
463
639
  const { child, logFile } = Service.spawnDetached(ctx.aseDir, port, this.log.logLevel());
464
640
  let exited = false;
465
641
  let exitCode = null;
@@ -524,12 +700,12 @@ export default class ServiceCommand {
524
700
  }
525
701
  throw lastErr;
526
702
  }
527
- /* build the "POST /command" request options for a command token */
528
- commandRequest(cmd, timeoutMs) {
703
+ /* build the JSON "POST" request options for a service endpoint */
704
+ postRequest(body, timeoutMs) {
529
705
  return {
530
706
  method: "POST",
531
707
  headers: { "Content-Type": "application/json" },
532
- body: { command: cmd },
708
+ body,
533
709
  signal: AbortSignal.timeout(timeoutMs),
534
710
  ignoreResponseError: true
535
711
  };
@@ -543,7 +719,7 @@ export default class ServiceCommand {
543
719
  }
544
720
  const match = await probe(ctx.port, ctx.projectId).catch(() => null);
545
721
  if (match === true) {
546
- const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
722
+ const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.postRequest({ command: "status" }, 2000));
547
723
  const d = r._data;
548
724
  const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
549
725
  const uptime = prettyMs(uptimeMs, { verbose: true });
@@ -569,13 +745,18 @@ export default class ServiceCommand {
569
745
  if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
570
746
  throw new Error(`service not responding on port ${ctx.port} after auto-start`);
571
747
  }
572
- const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
748
+ const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.postRequest({ command: cmd }, 5000), responseType: "text" });
573
749
  const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
574
750
  process.stdout.write(body);
575
751
  if (!body.endsWith("\n"))
576
752
  process.stdout.write("\n");
577
753
  return r.status >= 200 && r.status < 300 ? 0 : 1;
578
754
  }
755
+ /* log flow: show the (tail of the) log file and optionally follow it */
756
+ async doLog(opts) {
757
+ const ctx = this.loadContext();
758
+ return Service.showLog(path.join(ctx.aseDir, "service.log"), opts);
759
+ }
579
760
  /* stop flow: no-op if no port configured or connection refused */
580
761
  async doStop() {
581
762
  const ctx = this.loadContext();
@@ -593,11 +774,7 @@ export default class ServiceCommand {
593
774
  Service.clearPort(ctx.svc);
594
775
  return 0;
595
776
  }
596
- const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, {
597
- method: "GET",
598
- signal: AbortSignal.timeout(5000),
599
- ignoreResponseError: true
600
- });
777
+ const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, this.postRequest({}, 5000));
601
778
  const ok = r.status >= 200 && r.status < 300;
602
779
  Service.clearPort(ctx.svc);
603
780
  return ok ? 0 : 1;
@@ -615,9 +792,11 @@ export default class ServiceCommand {
615
792
  /* register CLI sub-command "ase service start" */
616
793
  service
617
794
  .command("start")
618
- .description("Start the background service")
619
- .action(async () => {
620
- process.exit(await this.doStart());
795
+ .description("Start the background service (or run it in the foreground)")
796
+ .option("-f, --foreground", "run the service in the foreground, logging to the terminal, " +
797
+ "until it is interrupted or stopped (without idle timeout)")
798
+ .action(async (opts) => {
799
+ process.exit(await this.doStart(opts));
621
800
  });
622
801
  /* register CLI sub-command "ase service status" */
623
802
  service
@@ -634,6 +813,15 @@ export default class ServiceCommand {
634
813
  .action(async (cmd) => {
635
814
  process.exit(await this.doSend(cmd));
636
815
  });
816
+ /* register CLI sub-command "ase service log" */
817
+ service
818
+ .command("log")
819
+ .description("Show the log of the background service")
820
+ .option("-f, --follow", "keep following the log for newly appended lines")
821
+ .option("-n, --lines <n>", "show the last <n> lines only (default: all, or 10 with --follow)")
822
+ .action(async (opts) => {
823
+ process.exit(await this.doLog(opts));
824
+ });
637
825
  /* register CLI sub-command "ase service stop" */
638
826
  service
639
827
  .command("stop")