@rse/ase 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -0,0 +1,351 @@
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 * as v from "valibot";
7
+ import { foregroundColorNames } from "chalk";
8
+ import { checkIdScheme } from "./ase-task-format.js";
9
+ import { parseEffortScale } from "./ase-task-format.js";
10
+ import { EFFORT_SCALE } from "./ase-task-format.js";
11
+ /* classification taxonomy */
12
+ export const projectClassification = {
13
+ boxing: ["white", "grey", "black"],
14
+ lifecycle: ["solo", "team", "enterprise"]
15
+ };
16
+ /* agent classification taxonomy */
17
+ export const agentClassification = {
18
+ persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"],
19
+ guidance: ["none", "minimal", "normal", "verbose"]
20
+ };
21
+ /* the default colors of the four color roles of the terminal board
22
+ ("default" is the foreground color of the terminal) */
23
+ export const tuiColorDefaults = {
24
+ dim: "grey",
25
+ normal: "default",
26
+ accent: "blue",
27
+ signal: "red"
28
+ };
29
+ /* the default base colors of the four color roles of the web board */
30
+ export const webColorDefaults = {
31
+ dim: "grey",
32
+ normal: "black",
33
+ accent: "blue",
34
+ signal: "orange"
35
+ };
36
+ /* classification presets */
37
+ export const projectClassificationPresets = {
38
+ vibe: {
39
+ "agent.persona": "writer",
40
+ "project.name": "Example Project",
41
+ "project.boxing": "black"
42
+ },
43
+ pro: {
44
+ "agent.persona": "engineer",
45
+ "project.name": "Example Project",
46
+ "project.boxing": "white"
47
+ },
48
+ default: {
49
+ "agent.persona": "engineer",
50
+ "agent.guidance": "normal",
51
+ "project.name": "Example Project",
52
+ "project.boxing": "white",
53
+ "project.users": "",
54
+ "project.task.lifecycle": "solo",
55
+ "project.task.idscheme": "slug",
56
+ "project.task.store.url": "ase:./.ase/task",
57
+ "project.task.default.source": "worktree",
58
+ "project.task.default.changeset": "worktree",
59
+ "project.task.default.target": "source",
60
+ "project.task.effort.scale": EFFORT_SCALE,
61
+ "project.task.effort.factor": "1",
62
+ "project.task.effort.file": "1",
63
+ "project.task.effort.line": "0.025",
64
+ "project.task.effort.iteration": "2.5",
65
+ "project.artifact.spec.basedir": "docs/specbook",
66
+ "project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
67
+ "project.artifact.spec.schema": "",
68
+ "project.artifact.code.basedir": "src",
69
+ "project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
70
+ "project.artifact.docs.basedir": "doc",
71
+ "project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
72
+ "project.artifact.infr.basedir": "",
73
+ "project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}",
74
+ "board.tui.color.dim": tuiColorDefaults.dim,
75
+ "board.tui.color.normal": tuiColorDefaults.normal,
76
+ "board.tui.color.accent": tuiColorDefaults.accent,
77
+ "board.tui.color.signal": tuiColorDefaults.signal,
78
+ "board.web.color.dim": webColorDefaults.dim,
79
+ "board.web.color.normal": webColorDefaults.normal,
80
+ "board.web.color.accent": webColorDefaults.accent,
81
+ "board.web.color.signal": webColorDefaults.signal,
82
+ "board.web.editor.keymap": "default",
83
+ "telemetry.enabled": "false",
84
+ "telemetry.implicit": "true",
85
+ "telemetry.prices.ai": "*=4.00/20.00/0.20/5.00",
86
+ "telemetry.prices.hr": "*=110"
87
+ },
88
+ industry: {
89
+ "agent.persona": "engineer",
90
+ "project.name": "Example Project",
91
+ "project.boxing": "grey"
92
+ }
93
+ };
94
+ /* hard-coded map: which scope kinds each variable may be SET on
95
+ (reads always cascade through the full chain; this restricts writes only);
96
+ keys absent from this map default to all non-"default" scope kinds */
97
+ export const configWritableScopes = {
98
+ "project.users": ["user", "project"],
99
+ "project.task.store.url": ["user", "project"],
100
+ "project.task.store.token": ["user"],
101
+ "project.task.store.crt": ["user", "project"],
102
+ "project.task.store.key": ["user", "project"],
103
+ "project.task.lifecycle": ["user", "project"],
104
+ "project.task.idscheme": ["user", "project"],
105
+ "project.task.default.source": ["user", "project"],
106
+ "project.task.default.changeset": ["user", "project"],
107
+ "project.task.default.target": ["user", "project"],
108
+ "project.task.effort.scale": ["user", "project"],
109
+ "project.task.effort.factor": ["user", "project"],
110
+ "project.task.effort.file": ["user", "project"],
111
+ "project.task.effort.line": ["user", "project"],
112
+ "project.task.effort.iteration": ["user", "project"],
113
+ "project.relay.url": ["user", "project"],
114
+ "project.relay.token": ["user"],
115
+ "project.relay.crt": ["user", "project"],
116
+ "project.relay.key": ["user", "project"],
117
+ "project.artifact.spec.basedir": ["user", "project"],
118
+ "project.artifact.spec.files": ["user", "project"],
119
+ "project.artifact.spec.schema": ["user", "project"],
120
+ "project.artifact.code.basedir": ["user", "project"],
121
+ "project.artifact.code.files": ["user", "project"],
122
+ "project.artifact.docs.basedir": ["user", "project"],
123
+ "project.artifact.docs.files": ["user", "project"],
124
+ "project.artifact.infr.basedir": ["user", "project"],
125
+ "project.artifact.infr.files": ["user", "project"],
126
+ "board.tui.color.dim": ["user", "project"],
127
+ "board.tui.color.normal": ["user", "project"],
128
+ "board.tui.color.accent": ["user", "project"],
129
+ "board.tui.color.signal": ["user", "project"],
130
+ "board.web.color.dim": ["user", "project"],
131
+ "board.web.color.normal": ["user", "project"],
132
+ "board.web.color.accent": ["user", "project"],
133
+ "board.web.color.signal": ["user", "project"],
134
+ "board.web.editor.keymap": ["user", "project"],
135
+ "telemetry.enabled": ["user", "project"],
136
+ "telemetry.implicit": ["user", "project"],
137
+ "telemetry.service.port": ["user"],
138
+ "telemetry.service.token": ["user"],
139
+ "telemetry.service.interval": ["user", "project"],
140
+ "telemetry.service.idle": ["user", "project"],
141
+ "telemetry.prices.ai": ["user"],
142
+ "telemetry.prices.hr": ["user", "project"]
143
+ };
144
+ /* default set of scope kinds writable for any unrestricted key */
145
+ export const configWritableScopesDefault = ["user", "project", "task", "session"];
146
+ /* keys carrying secrets, whose values are masked in listings */
147
+ export const configSecretKeys = ["project.task.store.token", "project.relay.token", "telemetry.service.token"];
148
+ /* parse the per-model price table "<model>=<input>/<output>[/<cread>/<cwrite>][ ...]"
149
+ of the AI usage accounting (USD per million input, output, cache read, and cache
150
+ write tokens, where missing cache prices fall back to the input price), keyed by model */
151
+ export const parsePrices = (spec) => {
152
+ const prices = new Map();
153
+ for (const entry of spec.split(/\s+/).filter((e) => e !== "")) {
154
+ const m = /^([^=\s]+)=(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)(?:\/(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?))?$/.exec(entry);
155
+ if (m === null)
156
+ throw new Error(`invalid price entry "${entry}"`);
157
+ prices.set(m[1], {
158
+ input: Number(m[2]),
159
+ output: Number(m[3]),
160
+ cread: Number(m[4] ?? m[2]),
161
+ cwrite: Number(m[5] ?? m[2])
162
+ });
163
+ }
164
+ return prices;
165
+ };
166
+ /* the price of a model (or tool): its own one, else the one of "*", else none */
167
+ export const modelPrice = (prices, model) => prices.get(model) ?? prices.get("*");
168
+ /* the cost (in USD) of token counts at a price (zero without a price) */
169
+ export const tokenCost = (price, tokens) => price === undefined ? 0 : (tokens.cread * price.cread + tokens.cwrite * price.cwrite +
170
+ tokens.input * price.input + tokens.output * price.output) / 1e6;
171
+ /* check whether a specification is accepted by its parser */
172
+ const parsable = (parse, spec) => {
173
+ try {
174
+ parse(spec);
175
+ return true;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ };
181
+ /* parse the per-user hourly rate table "<user-id>=<usd-per-hour>[,...]" of the
182
+ human time of the AI usage accounting, keyed by user id (with "*" for all others) */
183
+ export const parseRates = (spec) => {
184
+ const rates = new Map();
185
+ for (const entry of spec.split(",").map((e) => e.trim()).filter((e) => e !== "")) {
186
+ const m = /^([^=,\s]+)=(\d+(?:\.\d+)?)$/.exec(entry);
187
+ if (m === null)
188
+ throw new Error(`invalid hourly rate entry "${entry}"`);
189
+ rates.set(m[1], Number(m[2]));
190
+ }
191
+ return rates;
192
+ };
193
+ /* the hourly rate (in USD) of a user: its own one, else the one of "*", else zero */
194
+ export const userRate = (rates, user) => rates.get(user) ?? rates.get("*") ?? 0;
195
+ /* schema for a single artifact kind's "basedir"/"files" specification */
196
+ const artifactSchema = v.optional(v.strictObject({
197
+ basedir: v.optional(v.string()),
198
+ files: v.optional(v.string())
199
+ }));
200
+ /* schema for the "spec" artifact kind, additionally carrying the
201
+ whitespace-separated list of SpecBook YAML "schema" configuration
202
+ files, with "std" naming the bundled standard one (empty: "std") */
203
+ const artifactSpecSchema = v.optional(v.strictObject({
204
+ basedir: v.optional(v.string()),
205
+ files: v.optional(v.string()),
206
+ schema: v.optional(v.string())
207
+ }));
208
+ /* schema for a terminal color: "default" (the foreground color of the
209
+ terminal), a color name, "#rgb", "#rrggbb", "ansi256(<n>)", or "rgb(<r>,<g>,<b>)" */
210
+ const colorSchema = v.optional(v.pipe(v.string(), v.check((s) => s === "default"
211
+ || foregroundColorNames.includes(s)
212
+ || /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s)
213
+ || /^ansi256\(\s?\d+\s?\)$/.test(s)
214
+ || /^rgb\(\s?\d+,\s?\d+,\s?\d+\s?\)$/.test(s), "expected \"default\", a color name, \"#rrggbb\", \"ansi256(<n>)\", or \"rgb(<r>,<g>,<b>)\"")));
215
+ /* the color names accepted for the web board, mapped onto pleasant base colors */
216
+ export const webColorNames = {
217
+ black: "#1a1a1a",
218
+ grey: "#999999",
219
+ gray: "#999999",
220
+ brown: "#8a5a3c",
221
+ red: "#b03a30",
222
+ orange: "#b06820",
223
+ yellow: "#b09020",
224
+ green: "#3d8a3d",
225
+ teal: "#2a8a7a",
226
+ cyan: "#2a8aa0",
227
+ blue: "#336699",
228
+ purple: "#6a4a99",
229
+ magenta: "#993d80"
230
+ };
231
+ /* schema for a web color: "default" (the default base color of the role),
232
+ a color name, "#rgb", or "#rrggbb" */
233
+ const webColorSchema = v.optional(v.pipe(v.string(), v.check((s) => s === "default"
234
+ || Object.hasOwn(webColorNames, s)
235
+ || /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s), `expected "default", a color name (${Object.keys(webColorNames).join(", ")}), "#rgb", or "#rrggbb"`)));
236
+ /* schema for a default value of the "Source", "Changeset", or "Target" task
237
+ frontmatter key, restricted to the permitted forms "worktree", "source",
238
+ "branch:<name>" (a safe Git branch name), and "attachment:<name>", where a
239
+ <name> can carry the placeholder "{id}" of the task id (checked with a sample id) */
240
+ const flowSchema = (forms) => v.optional(v.pipe(v.string(), v.check((value) => {
241
+ const s = value.replaceAll("{id}", "id");
242
+ const i = s.indexOf(":");
243
+ const type = i < 0 ? s : s.slice(0, i);
244
+ const name = i < 0 ? "" : s.slice(i + 1);
245
+ if (!forms.includes(type))
246
+ return false;
247
+ else if (type === "branch")
248
+ return /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(name) && !/(?:\/|\.|\.lock)$/.test(name) && !/\.\.|\/\//.test(name);
249
+ else if (type === "attachment")
250
+ return /^[A-Za-z0-9_-]+$/.test(name);
251
+ else
252
+ return i < 0;
253
+ }, `expected ${forms.map((form) => form === "branch" || form === "attachment" ? `"${form}:<name>"` : `"${form}"`).join(", ")}`)));
254
+ /* schema for a positive number of human minutes of the effort estimation */
255
+ const minutesSchema = v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+(?:\.\d+)?$/.test(String(s)) && Number(s) > 0, "expected a positive number of minutes")));
256
+ /* schema for a multiplier of the effort estimation */
257
+ const multiplierSchema = v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+(?:\.\d+)?$/.test(String(s)) && Number(s) >= 1, "expected a multiplier of at least 1")));
258
+ /* schema for ".ase/config.yaml" */
259
+ export const configSchema = v.nullish(v.strictObject({
260
+ project: v.optional(v.strictObject({
261
+ id: v.optional(v.pipe(v.string(), v.minLength(1))),
262
+ name: v.optional(v.pipe(v.string(), v.minLength(1))),
263
+ boxing: v.optional(v.picklist(projectClassification.boxing)),
264
+ users: v.optional(v.string()),
265
+ task: v.optional(v.strictObject({
266
+ lifecycle: v.optional(v.picklist(projectClassification.lifecycle)),
267
+ store: v.optional(v.strictObject({
268
+ url: v.optional(v.pipe(v.string(), v.minLength(1))),
269
+ token: v.optional(v.pipe(v.string(), v.minLength(1))),
270
+ crt: v.optional(v.pipe(v.string(), v.minLength(1))),
271
+ key: v.optional(v.pipe(v.string(), v.minLength(1)))
272
+ })),
273
+ idscheme: v.optional(v.pipe(v.string(), v.check((s) => checkIdScheme(s) === "", "expected \"slug[:<words>]\", \"seq[:<template>]\", or \"any\""))),
274
+ default: v.optional(v.strictObject({
275
+ source: flowSchema(["worktree", "branch"]),
276
+ changeset: flowSchema(["worktree", "branch", "attachment"]),
277
+ target: flowSchema(["worktree", "branch", "source"])
278
+ })),
279
+ effort: v.optional(v.strictObject({
280
+ scale: v.optional(v.pipe(v.string(), v.check((s) => parsable(parseEffortScale, s), "expected \"<xs>,<s>,<m>,<l>\" (strictly ascending upper band bounds in minutes)"))),
281
+ factor: minutesSchema,
282
+ file: minutesSchema,
283
+ line: minutesSchema,
284
+ iteration: multiplierSchema
285
+ }))
286
+ })),
287
+ relay: v.optional(v.strictObject({
288
+ url: v.optional(v.pipe(v.string(), v.minLength(1))),
289
+ token: v.optional(v.pipe(v.string(), v.minLength(1))),
290
+ crt: v.optional(v.pipe(v.string(), v.minLength(1))),
291
+ key: v.optional(v.pipe(v.string(), v.minLength(1)))
292
+ })),
293
+ artifact: v.optional(v.strictObject({
294
+ spec: artifactSpecSchema,
295
+ code: artifactSchema,
296
+ docs: artifactSchema,
297
+ infr: artifactSchema
298
+ }))
299
+ })),
300
+ agent: v.optional(v.strictObject({
301
+ persona: v.optional(v.picklist(agentClassification.persona)),
302
+ guidance: v.optional(v.picklist(agentClassification.guidance))
303
+ })),
304
+ telemetry: v.optional(v.strictObject({
305
+ enabled: v.optional(v.union([v.boolean(), v.picklist(["true", "false"])])),
306
+ implicit: v.optional(v.union([v.boolean(), v.picklist(["true", "false"])])),
307
+ service: v.optional(v.strictObject({
308
+ port: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)) && Number(s) >= 1024 && Number(s) <= 65535, "expected a port number (1024-65535)"))),
309
+ token: v.optional(v.pipe(v.string(), v.minLength(1))),
310
+ interval: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)), "expected a number of minutes (0 disables)"))),
311
+ idle: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)), "expected a number of minutes (0 disables)")))
312
+ })),
313
+ prices: v.optional(v.strictObject({
314
+ ai: v.optional(v.pipe(v.string(), v.check((s) => parsable(parsePrices, s), "expected \"<model>=<input>/<output>[/<cread>/<cwrite>][ ...]\" (USD per million tokens, with \"*\" as the model of all others)"))),
315
+ hr: v.optional(v.pipe(v.string(), v.check((s) => parsable(parseRates, s), "expected \"<user-id>=<usd-per-hour>[,...]\" (with \"*\" as the user id of all others)")))
316
+ }))
317
+ })),
318
+ board: v.optional(v.strictObject({
319
+ tui: v.optional(v.strictObject({
320
+ color: v.optional(v.strictObject({
321
+ dim: colorSchema,
322
+ normal: colorSchema,
323
+ accent: colorSchema,
324
+ signal: colorSchema
325
+ }))
326
+ })),
327
+ web: v.optional(v.strictObject({
328
+ color: v.optional(v.strictObject({
329
+ dim: webColorSchema,
330
+ normal: webColorSchema,
331
+ accent: webColorSchema,
332
+ signal: webColorSchema
333
+ })),
334
+ editor: v.optional(v.strictObject({
335
+ keymap: v.optional(v.picklist(["default", "vim", "emacs"]))
336
+ }))
337
+ }))
338
+ }))
339
+ }));
340
+ /* schema for the session-scoped internal "state.yaml", carrying the machine-local
341
+ runtime state shared between the hooks, the MCP server, and the skills, which is
342
+ intentionally not part of the user-facing configuration: "basedir" is the project
343
+ directory, "task" is the id of the active
344
+ task, "skill" is the id of the active edit-capable skill, and "headless"
345
+ whether the session is a headless one of an agent */
346
+ export const sessionStateSchema = v.nullish(v.strictObject({
347
+ basedir: v.optional(v.pipe(v.string(), v.minLength(1))),
348
+ task: v.optional(v.pipe(v.string(), v.minLength(1))),
349
+ skill: v.optional(v.pipe(v.string(), v.minLength(1))),
350
+ headless: v.optional(v.picklist(["true", "false"]))
351
+ }));
@@ -0,0 +1,172 @@
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 os from "node:os";
8
+ import fs from "node:fs";
9
+ import { execaSync } from "execa";
10
+ import { LRUCache } from "lru-cache";
11
+ /* canonical ordering rank of a scope kind */
12
+ const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
13
+ /* parse a single scope term */
14
+ const parseScopeTerm = (value) => {
15
+ if (value === "user")
16
+ return { kind: "user" };
17
+ else if (value === "project")
18
+ return { kind: "project" };
19
+ const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
20
+ if (m !== null) {
21
+ if (m[2] === "." || m[2] === "..")
22
+ throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
23
+ return { kind: m[1], id: m[2] };
24
+ }
25
+ throw new Error(`invalid --scope term "${value}" ` +
26
+ "(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
27
+ };
28
+ /* determine the Git top-level directory, if inside a Git repository;
29
+ cached per working directory ("" ≡ not inside a Git working tree), as
30
+ each determination spawns a Git subprocess and the Git context can
31
+ change over the lifetime of the long-running ASE service */
32
+ const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
33
+ const gitToplevel = () => {
34
+ const cwd = process.cwd();
35
+ const cached = gitToplevelCache.get(cwd);
36
+ if (cached !== undefined)
37
+ return cached === "" ? null : cached;
38
+ let top = "";
39
+ try {
40
+ top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
41
+ }
42
+ catch {
43
+ /* not inside a Git working tree */
44
+ }
45
+ gitToplevelCache.set(cwd, top);
46
+ return top === "" ? null : top;
47
+ };
48
+ /* resolve the per-OS user-scope configuration directory */
49
+ export const userConfigDir = () => {
50
+ if (process.platform === "darwin")
51
+ /* macOS */
52
+ return path.join(os.homedir(), "Library", "Application Support", "ase");
53
+ else if (process.platform === "win32")
54
+ /* Windows (roaming) */
55
+ return path.join(process.env.APPDATA ?? os.homedir(), "ase");
56
+ else {
57
+ /* Linux */
58
+ const xdg = process.env.XDG_CONFIG_HOME;
59
+ const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".config");
60
+ return path.join(base, "ase");
61
+ }
62
+ };
63
+ /* resolve the per-OS user-scope state directory (per-session state and
64
+ other machine-local runtime information, never roamed or versioned) */
65
+ export const userStateDir = () => {
66
+ if (process.platform === "darwin")
67
+ /* macOS */
68
+ return path.join(os.homedir(), "Library", "Application Support", "ase");
69
+ else if (process.platform === "win32")
70
+ /* Windows (local) */
71
+ return path.join(process.env.LOCALAPPDATA ?? process.env.APPDATA ?? os.homedir(), "ase");
72
+ else {
73
+ /* Linux */
74
+ const xdg = process.env.XDG_STATE_HOME;
75
+ const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".local", "state");
76
+ return path.join(base, "ase");
77
+ }
78
+ };
79
+ /* ensure a self-ignoring ".ase/.gitignore" covering the machine-local runtime
80
+ files, so they never show up as untracked files in any project; an existing
81
+ file is left untouched, as the project may maintain its own rules */
82
+ export const ensureAseGitignore = (aseDir) => {
83
+ const file = path.join(aseDir, ".gitignore");
84
+ if (fs.existsSync(file))
85
+ return;
86
+ const rules = [".gitignore", "*.lock", "agent/", "board.yaml", "service.log", "service.yaml", "otel-capture/", "worktree/"];
87
+ try {
88
+ fs.mkdirSync(aseDir, { recursive: true });
89
+ fs.writeFileSync(file, `# ASE machine-local runtime files (generated)\n${rules.join("\n")}\n`, { encoding: "utf8", flag: "wx" });
90
+ }
91
+ catch {
92
+ /* intentionally ignore errors (e.g. concurrent creation or read-only
93
+ project), as a missing ignore file must never block the operation */
94
+ }
95
+ };
96
+ /* determine the project root directory, i.e. either the top-level
97
+ directory of the Git working tree or the nearest directory at or
98
+ above cwd which carries a ".ase" directory (excluding the home
99
+ directory, where a stale "~/.ase" of older ASE versions may exist) */
100
+ export const projectRoot = () => {
101
+ const top = gitToplevel();
102
+ if (top !== null)
103
+ return top;
104
+ let dir = fs.realpathSync(process.cwd());
105
+ const home = fs.realpathSync(os.homedir());
106
+ for (;;) {
107
+ if (dir !== home && fs.existsSync(path.join(dir, ".ase")))
108
+ return dir;
109
+ const parent = path.dirname(dir);
110
+ if (parent === dir)
111
+ return null;
112
+ dir = parent;
113
+ }
114
+ };
115
+ /* derive the fallback project id from the sanitized basename of a
116
+ project root (shared by task store, hook, service, and statusline);
117
+ for a multi-worktree setup "<basedir>/<branch>/" with an "active" symlink
118
+ in "<basedir>/" (pointing to one of the worktrees) the id is
119
+ "<basedir>-<branch>", as the bare branch name (e.g. "master") is not unique */
120
+ export const projectIdOf = (root) => {
121
+ let name = path.basename(root);
122
+ const parent = path.dirname(root);
123
+ if (name !== "" && parent !== root) {
124
+ try {
125
+ if (fs.lstatSync(path.join(parent, "active")).isSymbolicLink())
126
+ name = `${path.basename(parent)}-${name}`;
127
+ }
128
+ catch {
129
+ /* no "active" symlink, so no multi-worktree setup */
130
+ }
131
+ }
132
+ return name.replace(/[^A-Za-z0-9_-]/g, "_") || "project";
133
+ };
134
+ /* detect whether a project context exists, i.e. either we are inside
135
+ a Git working tree or a ".ase" directory is present at or above cwd */
136
+ const hasProjectContext = () => projectRoot() !== null;
137
+ /* parse a raw "--scope" option value into a canonical Scope chain;
138
+ accepts a comma-separated list of terms in any order. The "user"
139
+ term is always implicitly added at the bottom of the chain; the
140
+ "project" term is implicitly added only when a project context
141
+ exists (Git repository or ".ase" directory at or above cwd) and it
142
+ stays weaker than the strongest explicitly requested term, and an
143
+ explicit "project" term requires that same context */
144
+ export const parseScope = (value) => {
145
+ const projectActive = hasProjectContext();
146
+ const input = (value === undefined || value === "") ?
147
+ (projectActive ? "project" : "user") :
148
+ value.trim();
149
+ if (input === "")
150
+ throw new Error("invalid --scope: value must not be empty");
151
+ const terms = input.split(",").map((s) => parseScopeTerm(s.trim()));
152
+ const seen = new Set();
153
+ for (const t of terms) {
154
+ if (seen.has(t.kind))
155
+ throw new Error(`invalid --scope: duplicate term of kind "${t.kind}"`);
156
+ seen.add(t.kind);
157
+ }
158
+ if (seen.has("project") && !projectActive)
159
+ throw new Error("invalid --scope: \"project\" requires a project context " +
160
+ "(a Git repository or a \".ase\" directory at or above the current directory)");
161
+ /* the strongest term of the chain is the write target, so an implicitly
162
+ added "project" term must never outrank the strongest explicitly
163
+ requested term, as this would silently retarget the caller's request */
164
+ const rankMax = Math.max(...terms.map((t) => scopeRank(t.kind)));
165
+ if (!seen.has("project") && projectActive && rankMax > scopeRank("project"))
166
+ terms.unshift({ kind: "project" });
167
+ if (!seen.has("user"))
168
+ terms.unshift({ kind: "user" });
169
+ terms.sort((a, b) => scopeRank(a.kind) - scopeRank(b.kind));
170
+ terms.unshift({ kind: "default" });
171
+ return terms;
172
+ };