@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
package/dst/ase-config.js CHANGED
@@ -4,584 +4,16 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import path from "node:path";
7
- import os from "node:os";
8
7
  import fs from "node:fs";
9
8
  import readline from "node:readline/promises";
10
- import { Document, parseDocument, isMap, isScalar } from "yaml";
9
+ import { isMap, isScalar } from "yaml";
11
10
  import { execaSync } from "execa";
12
- import * as v from "valibot";
13
- import writeFileAtomic from "write-file-atomic";
14
- import lockfile from "proper-lockfile";
15
11
  import { z } from "zod";
16
- import { LRUCache } from "lru-cache";
17
- import { writeStdout } from "./ase-stdio.js";
18
- import { renderTable } from "./ase-table.js";
19
- /* classification taxonomy */
20
- export const projectClassification = {
21
- boxing: ["white", "grey", "black"]
22
- };
23
- /* agent classification taxonomy */
24
- export const agentClassification = {
25
- persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"],
26
- guidance: ["none", "minimal", "normal", "verbose"]
27
- };
28
- /* classification presets */
29
- export const projectClassificationPresets = {
30
- vibe: {
31
- "agent.persona": "writer",
32
- "project.id": "example",
33
- "project.name": "Example Project",
34
- "project.boxing": "black"
35
- },
36
- pro: {
37
- "agent.persona": "engineer",
38
- "project.id": "example",
39
- "project.name": "Example Project",
40
- "project.boxing": "white"
41
- },
42
- default: {
43
- "agent.task": "default",
44
- "agent.persona": "engineer",
45
- "agent.guidance": "normal",
46
- "project.id": "example",
47
- "project.name": "Example Project",
48
- "project.boxing": "white",
49
- "project.artifact.task.basedir": ".ase/task",
50
- "project.artifact.task.files": "*.md",
51
- "project.artifact.spec.basedir": "docs/specbook",
52
- "project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
53
- "project.artifact.spec.schema": "",
54
- "project.artifact.code.basedir": "src",
55
- "project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
56
- "project.artifact.docs.basedir": "doc",
57
- "project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
58
- "project.artifact.infr.basedir": "",
59
- "project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}"
60
- },
61
- industry: {
62
- "agent.persona": "engineer",
63
- "project.id": "example",
64
- "project.name": "Example Project",
65
- "project.boxing": "grey"
66
- }
67
- };
68
- /* hard-coded map: which scope kinds each variable may be SET on
69
- (reads always cascade through the full chain; this restricts writes only);
70
- keys absent from this map default to all non-"default" scope kinds */
71
- export const configWritableScopes = {
72
- "agent.task": ["session"],
73
- "agent.skill": ["session"],
74
- "project.artifact.task.basedir": ["user", "project"],
75
- "project.artifact.task.files": ["user", "project"],
76
- "project.artifact.spec.basedir": ["user", "project"],
77
- "project.artifact.spec.files": ["user", "project"],
78
- "project.artifact.spec.schema": ["user", "project"],
79
- "project.artifact.code.basedir": ["user", "project"],
80
- "project.artifact.code.files": ["user", "project"],
81
- "project.artifact.docs.basedir": ["user", "project"],
82
- "project.artifact.docs.files": ["user", "project"],
83
- "project.artifact.infr.basedir": ["user", "project"],
84
- "project.artifact.infr.files": ["user", "project"]
85
- };
86
- /* default set of scope kinds writable for any unrestricted key */
87
- const configWritableScopesDefault = ["user", "project", "task", "session"];
88
- /* canonical ordering rank of a scope kind */
89
- const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
90
- /* parse a single scope term */
91
- const parseScopeTerm = (value) => {
92
- if (value === "user")
93
- return { kind: "user" };
94
- else if (value === "project")
95
- return { kind: "project" };
96
- const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
97
- if (m !== null) {
98
- if (m[2] === "." || m[2] === "..")
99
- throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
100
- return { kind: m[1], id: m[2] };
101
- }
102
- throw new Error(`invalid --scope term "${value}" ` +
103
- "(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
104
- };
105
- /* determine the Git top-level directory, if inside a Git repository;
106
- cached per working directory ("" ≡ not inside a Git working tree), as
107
- each determination spawns a Git subprocess and the Git context can
108
- change over the lifetime of the long-running ASE service */
109
- const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
110
- const gitToplevel = () => {
111
- const cwd = process.cwd();
112
- const cached = gitToplevelCache.get(cwd);
113
- if (cached !== undefined)
114
- return cached === "" ? null : cached;
115
- let top = "";
116
- try {
117
- top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
118
- }
119
- catch {
120
- /* not inside a Git working tree */
121
- }
122
- gitToplevelCache.set(cwd, top);
123
- return top === "" ? null : top;
124
- };
125
- /* determine the project root directory, i.e. either the top-level
126
- directory of the Git working tree or the nearest directory at or
127
- above cwd which carries a ".ase" directory */
128
- const projectRoot = () => {
129
- const top = gitToplevel();
130
- if (top !== null)
131
- return top;
132
- let dir = fs.realpathSync(process.cwd());
133
- for (;;) {
134
- if (fs.existsSync(path.join(dir, ".ase")))
135
- return dir;
136
- const parent = path.dirname(dir);
137
- if (parent === dir)
138
- return null;
139
- dir = parent;
140
- }
141
- };
142
- /* detect whether a project context exists, i.e. either we are inside
143
- a Git working tree or a ".ase" directory is present at or above cwd */
144
- const hasProjectContext = () => projectRoot() !== null;
145
- /* parse a raw "--scope" option value into a canonical Scope chain;
146
- accepts a comma-separated list of terms in any order. The "user"
147
- term is always implicitly added at the bottom of the chain; the
148
- "project" term is implicitly added only when a project context
149
- exists (Git repository or ".ase" directory at or above cwd) and it
150
- stays weaker than the strongest explicitly requested term, and an
151
- explicit "project" term requires that same context */
152
- export const parseScope = (value) => {
153
- const projectActive = hasProjectContext();
154
- const input = (value === undefined || value === "") ?
155
- (projectActive ? "project" : "user") :
156
- value.trim();
157
- if (input === "")
158
- throw new Error("invalid --scope: value must not be empty");
159
- const terms = input.split(",").map((s) => parseScopeTerm(s.trim()));
160
- const seen = new Set();
161
- for (const t of terms) {
162
- if (seen.has(t.kind))
163
- throw new Error(`invalid --scope: duplicate term of kind "${t.kind}"`);
164
- seen.add(t.kind);
165
- }
166
- if (seen.has("project") && !projectActive)
167
- throw new Error("invalid --scope: \"project\" requires a project context " +
168
- "(a Git repository or a \".ase\" directory at or above the current directory)");
169
- /* the strongest term of the chain is the write target, so an implicitly
170
- added "project" term must never outrank the strongest explicitly
171
- requested term, as this would silently retarget the caller's request */
172
- const rankMax = Math.max(...terms.map((t) => scopeRank(t.kind)));
173
- if (!seen.has("project") && projectActive && rankMax > scopeRank("project"))
174
- terms.unshift({ kind: "project" });
175
- if (!seen.has("user"))
176
- terms.unshift({ kind: "user" });
177
- terms.sort((a, b) => scopeRank(a.kind) - scopeRank(b.kind));
178
- terms.unshift({ kind: "default" });
179
- return terms;
180
- };
181
- /* schema for a single artifact kind's "basedir"/"files" specification */
182
- const artifactSchema = v.optional(v.strictObject({
183
- basedir: v.optional(v.string()),
184
- files: v.optional(v.string())
185
- }));
186
- /* schema for the "spec" artifact kind, additionally carrying the
187
- whitespace-separated list of SpecBook YAML "schema" configuration
188
- files, with "std" naming the bundled standard one (empty: "std") */
189
- const artifactSpecSchema = v.optional(v.strictObject({
190
- basedir: v.optional(v.string()),
191
- files: v.optional(v.string()),
192
- schema: v.optional(v.string())
193
- }));
194
- /* schema for ".ase/config.yaml" */
195
- export const configSchema = v.nullish(v.strictObject({
196
- project: v.optional(v.strictObject({
197
- id: v.optional(v.pipe(v.string(), v.minLength(1))),
198
- name: v.optional(v.pipe(v.string(), v.minLength(1))),
199
- boxing: v.optional(v.picklist(projectClassification.boxing)),
200
- artifact: v.optional(v.strictObject({
201
- spec: artifactSpecSchema,
202
- code: artifactSchema,
203
- docs: artifactSchema,
204
- infr: artifactSchema,
205
- task: artifactSchema
206
- }))
207
- })),
208
- agent: v.optional(v.strictObject({
209
- persona: v.optional(v.picklist(agentClassification.persona)),
210
- guidance: v.optional(v.picklist(agentClassification.guidance)),
211
- task: v.optional(v.pipe(v.string(), v.minLength(1))),
212
- skill: v.optional(v.pipe(v.string(), v.minLength(1)))
213
- }))
214
- }));
215
- /* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
216
- each associated with a scope term; reads cascade along user < project < task < session,
217
- writes are confined to the target (strongest) scope term */
218
- export class Config {
219
- /* public state */
220
- filename;
221
- /* private state */
222
- name;
223
- scope;
224
- schema;
225
- log;
226
- docs;
227
- target;
228
- pruned;
229
- /* creation */
230
- constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
231
- if (scope.length === 0)
232
- throw new Error("invalid scope: chain must not be empty");
233
- this.name = name;
234
- this.scope = scope[0].kind === "default" ? scope : [{ kind: "default" }, ...scope];
235
- this.schema = schema ?? null;
236
- this.log = log;
237
- const tgt = this.scope[this.scope.length - 1];
238
- this.filename = this.resolveFilename(name, tgt);
239
- this.docs = [{ scope: tgt, filename: this.filename, doc: new Document() }];
240
- this.target = 0;
241
- this.pruned = [];
242
- }
243
- /* render a scope term as a short textual label */
244
- static scopeLabel(term) {
245
- if (term.kind === "default" || term.kind === "user" || term.kind === "project")
246
- return term.kind;
247
- return `${term.kind}:${term.id}`;
248
- }
249
- /* resolve the per-OS user-scope base directory */
250
- userConfigDir() {
251
- if (process.platform === "darwin")
252
- /* macOS */
253
- return path.join(os.homedir(), "Library", "Application Support", "ase");
254
- else if (process.platform === "win32")
255
- /* Windows */
256
- return path.join(process.env.APPDATA ?? os.homedir(), "ase");
257
- else {
258
- /* Linux */
259
- const xdg = process.env.XDG_CONFIG_HOME;
260
- const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".config");
261
- return path.join(base, "ase");
262
- }
263
- }
264
- /* resolve the configuration filename based on the selected scope term */
265
- resolveFilename(name, term) {
266
- if (term.kind === "default")
267
- throw new Error("internal error: \"default\" scope has no filename");
268
- if (term.kind === "user")
269
- return path.join(this.userConfigDir(), `${name}.yaml`);
270
- else if (term.kind === "project") {
271
- const rel = path.join(".ase", `${name}.yaml`);
272
- const root = projectRoot() ?? process.cwd();
273
- const found = this.findUpward(process.cwd(), root, rel);
274
- return found ?? path.join(root, rel);
275
- }
276
- else if (term.kind === "task") {
277
- const root = projectRoot() ?? process.cwd();
278
- return path.join(root, ".ase", "task", term.id, `${name}.yaml`);
279
- }
280
- else
281
- return path.join(os.homedir(), ".ase", "session", term.id, `${name}.yaml`);
282
- }
283
- /* upward-walk on filesystem for a file path relative to a start directory,
284
- bounded above (inclusive) by a stop directory */
285
- findUpward(start, stop, rel) {
286
- let dir = fs.realpathSync(start);
287
- const end = fs.realpathSync(stop);
288
- const steps = dir.split(path.sep).length - end.split(path.sep).length;
289
- if (steps < 0)
290
- return null;
291
- for (let i = 0; i <= steps; i++) {
292
- const candidate = path.join(dir, rel);
293
- if (fs.existsSync(candidate))
294
- return candidate;
295
- const parent = path.dirname(dir);
296
- if (parent === dir)
297
- return null;
298
- dir = parent;
299
- }
300
- return null;
301
- }
302
- /* read the full scope chain into memory; the requested mode applies
303
- to the target scope only, inherited scopes are always lenient */
304
- read(mode = "lenient") {
305
- const chain = this.scope;
306
- const docs = [];
307
- this.pruned = [];
308
- for (let i = 0; i < chain.length; i++) {
309
- const sc = chain[i];
310
- if (sc.kind === "default") {
311
- const doc = new Document();
312
- doc.contents = doc.createNode({});
313
- if (this.name === "config") {
314
- const preset = projectClassificationPresets.default;
315
- for (const [k, val] of Object.entries(preset)) {
316
- const segments = k.split(".");
317
- for (let j = 1; j < segments.length; j++) {
318
- const prefix = segments.slice(0, j);
319
- const node = doc.getIn(prefix, true);
320
- if (node === undefined)
321
- doc.setIn(prefix, doc.createNode({}));
322
- }
323
- doc.setIn(segments, doc.createNode(val));
324
- }
325
- }
326
- docs.push({ scope: sc, filename: "", doc });
327
- continue;
328
- }
329
- const filename = this.resolveFilename(this.name, sc);
330
- const isTarget = (i === chain.length - 1);
331
- const perDocMode = isTarget ? mode : "lenient";
332
- const text = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : "";
333
- let doc = parseDocument(text);
334
- if (doc.errors.length > 0) {
335
- const msg = `invalid YAML in ${filename}: ${doc.errors[0].message}`;
336
- if (perDocMode === "strict")
337
- throw new Error(msg);
338
- this.log.write("warning", msg);
339
- if (isTarget)
340
- this.pruned.push(`unparsable YAML (${doc.errors[0].message.split("\n")[0]})`);
341
- doc = new Document();
342
- }
343
- docs.push({ scope: sc, filename, doc });
344
- }
345
- this.docs = docs;
346
- this.target = docs.length - 1;
347
- for (let i = 0; i < docs.length; i++) {
348
- const isTarget = (i === this.target);
349
- const perDocMode = isTarget ? mode : "lenient";
350
- const removed = this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
351
- if (isTarget)
352
- this.pruned.push(...removed);
353
- }
354
- }
355
- /* acquire a cross-process advisory lock on the target scope's file,
356
- execute the callback, then release the lock */
357
- lock(cb) {
358
- const td = this.docs[this.target];
359
- if (td.scope.kind === "default")
360
- throw new Error("internal error: \"default\" scope is not lockable");
361
- fs.mkdirSync(path.dirname(td.filename), { recursive: true });
362
- if (!fs.existsSync(td.filename))
363
- fs.writeFileSync(td.filename, "", "utf8");
364
- const release = lockfile.lockSync(td.filename);
365
- try {
366
- cb();
367
- }
368
- finally {
369
- release();
370
- }
371
- }
372
- /* write in-memory configuration back to the target scope's file */
373
- write() {
374
- const td = this.docs[this.target];
375
- if (td.scope.kind === "default")
376
- throw new Error("internal error: \"default\" scope is not writable");
377
- /* a lenient read physically removes the invalid content from the in-memory
378
- target document, so writing that document back would silently erase the
379
- very same content from the file on disk */
380
- if (this.pruned.length > 0)
381
- throw new Error(`refusing to overwrite ${td.filename}: it carries invalid content ` +
382
- `(${this.pruned.join(", ")}) which was skipped on reading and hence would be ` +
383
- "lost on writing -- repair the file first");
384
- this.validateDoc(td.doc, td.filename, "strict");
385
- fs.mkdirSync(path.dirname(td.filename), { recursive: true });
386
- writeFileAtomic.sync(td.filename, td.doc.toString({ indent: 4 }), { encoding: "utf8" });
387
- }
388
- /* validate a single YAML document against the optional schema; in "strict"
389
- mode all invalid entries are reported as a thrown error, in "lenient" mode
390
- they are removed from the document and their dotted paths are returned */
391
- validateDoc(doc, filename, mode = "strict") {
392
- if (this.schema === null)
393
- return [];
394
- const removed = [];
395
- for (;;) {
396
- const result = v.safeParse(this.schema, doc.toJS());
397
- if (result.success)
398
- return removed;
399
- if (mode === "strict") {
400
- const issues = result.issues.map((i) => {
401
- const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
402
- return dotPath ? `${dotPath}: ${i.message}` : i.message;
403
- }).join("; ");
404
- throw new Error(`invalid configuration in ${filename}: ${issues}`);
405
- }
406
- const before = removed.length;
407
- for (const i of result.issues) {
408
- const segs = (i.path ?? []).map((p) => String(p.key));
409
- const dotPath = segs.join(".");
410
- this.log.write("warning", `invalid entry in ${filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
411
- if (segs.length > 0 && doc.deleteIn(segs))
412
- removed.push(dotPath);
413
- /* issues at the document root and issues whose stringified path does not
414
- address a deletable node (e.g. a non-string YAML key like "404:", which
415
- "toJS" stringifies) cannot be removed; processing continues with the
416
- remaining issues */
417
- }
418
- if (removed.length === before)
419
- return removed;
420
- }
421
- }
422
- /* enumerate all full dotted leaf paths from the attached valibot schema */
423
- schemaLeafPaths() {
424
- const unwrap = (s) => {
425
- let cur = s;
426
- while (cur !== undefined && cur !== null && (cur.type === "optional" || cur.type === "nullish"
427
- || cur.type === "nullable" || cur.type === "undefinedable"))
428
- cur = cur.wrapped;
429
- return cur ?? null;
430
- };
431
- const walk = (s, prefix) => {
432
- const u = unwrap(s);
433
- if (u !== null
434
- && (u.type === "object" || u.type === "strict_object" || u.type === "loose_object")
435
- && u.entries !== undefined) {
436
- const paths = [];
437
- for (const [k, sub] of Object.entries(u.entries))
438
- paths.push(...walk(sub, [...prefix, k]));
439
- return paths;
440
- }
441
- return [prefix];
442
- };
443
- return walk(this.schema, []);
444
- }
445
- /* resolve a (possibly trailing-segment) dotted key to its full schema path */
446
- resolveKey(key) {
447
- if (this.schema === null)
448
- return key;
449
- const segs = key.split(".");
450
- const matches = this.schemaLeafPaths().filter((p) => {
451
- if (p.length < segs.length)
452
- return false;
453
- for (let i = 0; i < segs.length; i++)
454
- if (p[p.length - segs.length + i] !== segs[i])
455
- return false;
456
- return true;
457
- });
458
- if (matches.length === 0)
459
- return key;
460
- if (matches.length > 1)
461
- throw new Error(`ambiguous key "${key}" matches: ${matches.map((m) => m.join(".")).join(", ")}`);
462
- return matches[0].join(".");
463
- }
464
- /* retrieve the effective value at a dotted key (strongest scope wins),
465
- or the target scope's root contents if no key is given */
466
- get(key) {
467
- if (key === undefined)
468
- return this.docs[this.target].doc.contents;
469
- const segs = this.resolveKey(key).split(".");
470
- for (let i = this.docs.length - 1; i >= 0; i--) {
471
- const node = this.docs[i].doc.getIn(segs);
472
- if (node !== undefined)
473
- return node;
474
- }
475
- return undefined;
476
- }
477
- /* retrieve the explicitly configured value at a dotted key, i.e. the same
478
- cascade as "get", but skipping the built-in "default" scope layer, so
479
- callers can distinguish a deliberately configured value from a merely
480
- preset one */
481
- getExplicit(key) {
482
- const segs = this.resolveKey(key).split(".");
483
- for (let i = this.docs.length - 1; i >= 0; i--) {
484
- if (this.docs[i].scope.kind === "default")
485
- continue;
486
- const node = this.docs[i].doc.getIn(segs);
487
- if (node !== undefined)
488
- return node;
489
- }
490
- return undefined;
491
- }
492
- /* enumerate the effective leaf entries across the full scope chain;
493
- each returned entry identifies the originating scope */
494
- entries() {
495
- const keys = new Set();
496
- const walk = (node, prefix) => {
497
- if (isMap(node))
498
- for (const item of node.items) {
499
- const k = [...prefix, String(item.key)];
500
- if (isMap(item.value))
501
- walk(item.value, k);
502
- else if (isScalar(item.value))
503
- keys.add(k.join("."));
504
- else
505
- throw new Error(`key "${k.join(".")}" has unsupported node type`);
506
- }
507
- };
508
- for (const d of this.docs)
509
- walk(d.doc.contents, []);
510
- const result = [];
511
- for (const k of keys) {
512
- const segs = k.split(".");
513
- for (let i = this.docs.length - 1; i >= 0; i--) {
514
- const node = this.docs[i].doc.getIn(segs);
515
- if (node !== undefined) {
516
- result.push({ key: k, value: node, scope: this.docs[i].scope });
517
- break;
518
- }
519
- }
520
- }
521
- result.sort((a, b) => a.key.localeCompare(b.key));
522
- return result;
523
- }
524
- /* determine whether a key is writable on a given scope kind */
525
- isWritableOn(key, kind) {
526
- if (kind === "default")
527
- return false;
528
- const resolved = this.resolveKey(key);
529
- const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
530
- return allowed.includes(kind);
531
- }
532
- /* enforce write-scope policy for the current target scope */
533
- assertWritable(key) {
534
- const td = this.docs[this.target];
535
- const resolved = this.resolveKey(key);
536
- const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
537
- if (!allowed.includes(td.scope.kind))
538
- throw new Error(`cannot set "${resolved}" on scope "${Config.scopeLabel(td.scope)}": ` +
539
- `this key is only writable on scope(s): ${allowed.join(", ")}`);
540
- }
541
- /* set a value at a dotted key in the target scope, creating intermediate maps as needed */
542
- set(key, value) {
543
- this.assertWritable(key);
544
- const segments = this.resolveKey(key).split(".");
545
- const td = this.docs[this.target];
546
- const next = td.doc.clone();
547
- for (let i = 1; i < segments.length; i++) {
548
- const prefix = segments.slice(0, i);
549
- const node = next.getIn(prefix, true);
550
- if (node !== undefined && !isMap(node))
551
- throw new Error(`cannot set "${key}": intermediate path "${prefix.join(".")}" is not a map`);
552
- if (node === undefined)
553
- next.setIn(prefix, next.createNode({}));
554
- }
555
- next.setIn(segments, value);
556
- const saved = td.doc;
557
- td.doc = next;
558
- try {
559
- this.validateDoc(td.doc, td.filename, "strict");
560
- }
561
- catch (err) {
562
- td.doc = saved;
563
- throw err;
564
- }
565
- }
566
- /* delete a value at a dotted key from the target scope */
567
- delete(key) {
568
- this.assertWritable(key);
569
- const td = this.docs[this.target];
570
- if (td.doc.contents === null || td.doc.contents === undefined)
571
- return;
572
- const next = td.doc.clone();
573
- next.deleteIn(this.resolveKey(key).split("."));
574
- const saved = td.doc;
575
- td.doc = next;
576
- try {
577
- this.validateDoc(td.doc, td.filename, "strict");
578
- }
579
- catch (err) {
580
- td.doc = saved;
581
- throw err;
582
- }
583
- }
584
- }
12
+ import { writeStdout } from "./ase-lib-stdio.js";
13
+ import { renderTable } from "./ase-lib-table.js";
14
+ import { parseScope } from "./ase-config-scope.js";
15
+ import { projectClassificationPresets, configSecretKeys, configSchema } from "./ase-config-schema.js";
16
+ import { Config } from "./ase-config-core.js";
585
17
  /* CLI command "ase config" */
586
18
  export default class ConfigCommand {
587
19
  log;
@@ -628,17 +60,20 @@ export default class ConfigCommand {
628
60
  /* register CLI sub-command "ase config list" */
629
61
  configCmd
630
62
  .command("list")
631
- .description("list all configured values as flat dotted keys")
63
+ .description("list all available configuration keys and their effective values")
632
64
  .action(async (_opts, cmd) => {
633
65
  const scope = parseScope(cmd.optsWithGlobals().scope);
634
66
  const cfg = new Config("config", configSchema, this.log, scope);
635
67
  cfg.read();
636
- const rows = [];
68
+ const rows = new Map();
69
+ for (const key of cfg.keys())
70
+ rows.set(key, [key, "", "-"]);
637
71
  for (const e of cfg.entries()) {
638
72
  const val = isScalar(e.value) ? e.value.value : e.value;
639
- rows.push([e.key, String(val), Config.scopeLabel(e.scope)]);
73
+ rows.set(e.key, [e.key, configSecretKeys.includes(e.key) ? "***" : String(val), Config.scopeLabel(e.scope)]);
640
74
  }
641
- await writeStdout(renderTable(["KEY", "VALUE", "SCOPE"], rows));
75
+ const sorted = [...rows.values()].sort((a, b) => a[0].localeCompare(b[0]));
76
+ await writeStdout(renderTable(["KEY", "VALUE", "SCOPE"], sorted));
642
77
  });
643
78
  /* register CLI sub-command "ase config edit" */
644
79
  configCmd
@@ -646,15 +81,20 @@ export default class ConfigCommand {
646
81
  .description("edit configuration file with $EDITOR")
647
82
  .action(async (_opts, cmd) => {
648
83
  const scope = parseScope(cmd.optsWithGlobals().scope);
649
- const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
84
+ const editor = [process.env.VISUAL, process.env.EDITOR]
85
+ .find((e) => e !== undefined && e.trim() !== "") ?? "vi";
86
+ const [editorCmd, ...editorArgs] = editor.trim().split(/\s+/);
650
87
  const cfg = new Config("config", configSchema, this.log, scope);
651
88
  fs.mkdirSync(path.dirname(cfg.filename), { recursive: true });
652
89
  if (!fs.existsSync(cfg.filename))
653
- fs.writeFileSync(cfg.filename, "", "utf8");
90
+ fs.writeFileSync(cfg.filename, "", {
91
+ encoding: "utf8",
92
+ mode: scope[scope.length - 1].kind === "user" ? 0o600 : undefined
93
+ });
654
94
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
655
95
  try {
656
96
  for (;;) {
657
- execaSync(editor, [cfg.filename], { stdio: "inherit" });
97
+ execaSync(editorCmd, [...editorArgs, cfg.filename], { stdio: "inherit" });
658
98
  try {
659
99
  cfg.read("strict");
660
100
  break;
@@ -741,7 +181,7 @@ export class ConfigMCP {
741
181
  "returns an empty string if no value is set.",
742
182
  inputSchema: {
743
183
  key: z.string()
744
- .describe("dotted configuration key (e.g. \"agent.skill\")"),
184
+ .describe("dotted configuration key (e.g. \"agent.persona\")"),
745
185
  scope: z.string()
746
186
  .describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
747
187
  }
@@ -766,7 +206,7 @@ export class ConfigMCP {
766
206
  "the configuration schema before being persisted.",
767
207
  inputSchema: {
768
208
  key: z.string()
769
- .describe("dotted configuration key (e.g. \"agent.skill\")"),
209
+ .describe("dotted configuration key (e.g. \"agent.persona\")"),
770
210
  val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())])
771
211
  .describe("value to store under `key`"),
772
212
  scope: z.string()
@@ -794,7 +234,7 @@ export class ConfigMCP {
794
234
  "(the strongest scope term in the chain). No-op if the key is not present.",
795
235
  inputSchema: {
796
236
  key: z.string()
797
- .describe("dotted configuration key (e.g. \"agent.skill\")"),
237
+ .describe("dotted configuration key (e.g. \"agent.persona\")"),
798
238
  scope: z.string()
799
239
  .describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
800
240
  }
@@ -841,7 +281,7 @@ export class ConfigMCP {
841
281
  cfg.read();
842
282
  const entries = cfg.entries().map((e) => ({
843
283
  key: e.key,
844
- value: String(isScalar(e.value) ? e.value.value : e.value),
284
+ value: configSecretKeys.includes(e.key) ? "***" : String(isScalar(e.value) ? e.value.value : e.value),
845
285
  scope: Config.scopeLabel(e.scope)
846
286
  }));
847
287
  const result = { entries };