clanka 0.5.0 → 0.5.2
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.
- package/dist/Acp.js +5 -5
- package/dist/Acp.js.map +1 -1
- package/dist/Acp.test.js +96 -9
- package/dist/Acp.test.js.map +1 -1
- package/dist/AgentExecutor.d.ts.map +1 -1
- package/dist/AgentExecutor.js +1 -0
- package/dist/AgentExecutor.js.map +1 -1
- package/dist/AgentSkills.d.ts +5 -2
- package/dist/AgentSkills.d.ts.map +1 -1
- package/dist/AgentSkills.js +23 -6
- package/dist/AgentSkills.js.map +1 -1
- package/dist/AgentSkills.test.js +154 -7
- package/dist/AgentSkills.test.js.map +1 -1
- package/package.json +1 -1
- package/src/Acp.test.ts +139 -16
- package/src/Acp.ts +5 -5
- package/src/AgentExecutor.ts +1 -0
- package/src/AgentSkills.test.ts +336 -15
- package/src/AgentSkills.ts +26 -6
package/src/AgentSkills.test.ts
CHANGED
|
@@ -40,6 +40,7 @@ const withRoots = <A, E, R>(
|
|
|
40
40
|
f: (roots: {
|
|
41
41
|
readonly project: string
|
|
42
42
|
readonly home: string
|
|
43
|
+
readonly hermes: string
|
|
43
44
|
}) => Effect.Effect<A, E, R>,
|
|
44
45
|
) =>
|
|
45
46
|
Effect.scoped(
|
|
@@ -49,9 +50,11 @@ const withRoots = <A, E, R>(
|
|
|
49
50
|
const base = yield* fs.makeTempDirectoryScoped()
|
|
50
51
|
const project = path.join(base, "project")
|
|
51
52
|
const home = path.join(base, "home")
|
|
53
|
+
const hermes = path.join(base, "hermes-home")
|
|
54
|
+
yield* fs.makeDirectory(hermes)
|
|
52
55
|
yield* fs.makeDirectory(project)
|
|
53
56
|
yield* fs.makeDirectory(home)
|
|
54
|
-
return yield* f({ project, home })
|
|
57
|
+
return yield* f({ project, home, hermes })
|
|
55
58
|
}),
|
|
56
59
|
)
|
|
57
60
|
|
|
@@ -61,7 +64,291 @@ const discover = (roots: { readonly project: string; readonly home: string }) =>
|
|
|
61
64
|
homeDirectory: Option.some(roots.home),
|
|
62
65
|
})
|
|
63
66
|
|
|
67
|
+
const writeHermesSkill = Effect.fnUntraced(function* (
|
|
68
|
+
root: string,
|
|
69
|
+
dir: string,
|
|
70
|
+
content: string,
|
|
71
|
+
) {
|
|
72
|
+
const fs = yield* FileSystem.FileSystem
|
|
73
|
+
const path = yield* Path.Path
|
|
74
|
+
const skillDir = path.join(root, "skills", dir)
|
|
75
|
+
yield* fs.makeDirectory(skillDir, { recursive: true })
|
|
76
|
+
const location = path.join(skillDir, "SKILL.md")
|
|
77
|
+
yield* fs.writeFileString(location, content)
|
|
78
|
+
return location
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const discoverWithHermes = (
|
|
82
|
+
roots: {
|
|
83
|
+
readonly project: string
|
|
84
|
+
readonly home: string
|
|
85
|
+
readonly hermes: string
|
|
86
|
+
},
|
|
87
|
+
hermesHome: Option.Option<string> = Option.some(roots.hermes),
|
|
88
|
+
) =>
|
|
89
|
+
AgentSkills.discover({
|
|
90
|
+
directory: roots.project,
|
|
91
|
+
homeDirectory: Option.some(roots.home),
|
|
92
|
+
hermesHome,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const withLocalExecutor = <A, E, R>(
|
|
96
|
+
roots: { readonly project: string; readonly home: string },
|
|
97
|
+
hermesHome: string | undefined,
|
|
98
|
+
effect: Effect.Effect<A, E, R>,
|
|
99
|
+
) =>
|
|
100
|
+
Effect.scoped(
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
yield* Effect.acquireRelease(
|
|
103
|
+
Effect.sync(() => {
|
|
104
|
+
const previous = {
|
|
105
|
+
HOME: process.env.HOME,
|
|
106
|
+
HERMES_HOME: process.env.HERMES_HOME,
|
|
107
|
+
}
|
|
108
|
+
process.env.HOME = roots.home
|
|
109
|
+
if (hermesHome === undefined) delete process.env.HERMES_HOME
|
|
110
|
+
else process.env.HERMES_HOME = hermesHome
|
|
111
|
+
return previous
|
|
112
|
+
}),
|
|
113
|
+
(previous) =>
|
|
114
|
+
Effect.sync(() => {
|
|
115
|
+
for (const key of ["HOME", "HERMES_HOME"] as const) {
|
|
116
|
+
if (previous[key] === undefined) delete process.env[key]
|
|
117
|
+
else process.env[key] = previous[key]
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
return yield* effect.pipe(
|
|
122
|
+
Effect.provide(
|
|
123
|
+
AgentExecutor.layerLocal({ directory: roots.project }).pipe(
|
|
124
|
+
Layer.provide(NodeHttpClient.layerUndici),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
|
|
64
131
|
describe("AgentSkills", () => {
|
|
132
|
+
it.effect("catalogs a flat HERMES_HOME skill with an absolute location", () =>
|
|
133
|
+
withRoots(
|
|
134
|
+
Effect.fnUntraced(function* (roots) {
|
|
135
|
+
const path = yield* Path.Path
|
|
136
|
+
const location = yield* writeHermesSkill(
|
|
137
|
+
roots.hermes,
|
|
138
|
+
"bound",
|
|
139
|
+
skillFile("bound", "Workspace skill"),
|
|
140
|
+
)
|
|
141
|
+
const skills = yield* discoverWithHermes(roots)
|
|
142
|
+
assert.deepStrictEqual(
|
|
143
|
+
skills.map(({ name, description, location }) => [
|
|
144
|
+
name,
|
|
145
|
+
description,
|
|
146
|
+
location,
|
|
147
|
+
]),
|
|
148
|
+
[["bound", "Workspace skill", location]],
|
|
149
|
+
)
|
|
150
|
+
assert.isTrue(path.isAbsolute(skills[0]!.location))
|
|
151
|
+
}),
|
|
152
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for (const projectWins of [true, false]) {
|
|
156
|
+
it.effect(
|
|
157
|
+
projectWins
|
|
158
|
+
? "project skill overrides Hermes and user skills"
|
|
159
|
+
: "Hermes skill overrides a user skill",
|
|
160
|
+
() =>
|
|
161
|
+
withRoots(
|
|
162
|
+
Effect.fnUntraced(function* (roots) {
|
|
163
|
+
yield* writeSkill(roots.home, "shared", skillFile("shared", "User"))
|
|
164
|
+
const hermesLocation = yield* writeHermesSkill(
|
|
165
|
+
roots.hermes,
|
|
166
|
+
"shared",
|
|
167
|
+
skillFile("shared", "Hermes"),
|
|
168
|
+
)
|
|
169
|
+
const location = projectWins
|
|
170
|
+
? yield* writeSkill(
|
|
171
|
+
roots.project,
|
|
172
|
+
"shared",
|
|
173
|
+
skillFile("shared", "Project"),
|
|
174
|
+
)
|
|
175
|
+
: hermesLocation
|
|
176
|
+
const skills = yield* discoverWithHermes(roots)
|
|
177
|
+
assert.deepStrictEqual(
|
|
178
|
+
skills.map((skill) => [skill.description, skill.location]),
|
|
179
|
+
[[projectWins ? "Project" : "Hermes", location]],
|
|
180
|
+
)
|
|
181
|
+
}),
|
|
182
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const mode of ["unset", "empty", "missing skills"] as const) {
|
|
187
|
+
it.effect(`preserves existing discovery with ${mode} HERMES_HOME`, () =>
|
|
188
|
+
withRoots(
|
|
189
|
+
Effect.fnUntraced(function* (roots) {
|
|
190
|
+
const fs = yield* FileSystem.FileSystem
|
|
191
|
+
const path = yield* Path.Path
|
|
192
|
+
yield* writeSkill(roots.project, "proj", skillFile("proj", "Project"))
|
|
193
|
+
yield* writeSkill(roots.home, "usr", skillFile("usr", "User"))
|
|
194
|
+
// An unset input must not fall back to ~/.hermes.
|
|
195
|
+
yield* writeHermesSkill(
|
|
196
|
+
path.join(roots.home, ".hermes"),
|
|
197
|
+
"ignored",
|
|
198
|
+
skillFile("ignored", "Ignored"),
|
|
199
|
+
)
|
|
200
|
+
const reads: Array<string> = []
|
|
201
|
+
const skills = yield* discoverWithHermes(
|
|
202
|
+
roots,
|
|
203
|
+
mode === "unset"
|
|
204
|
+
? Option.none()
|
|
205
|
+
: Option.some(mode === "empty" ? "" : roots.hermes),
|
|
206
|
+
).pipe(
|
|
207
|
+
Effect.provideService(
|
|
208
|
+
FileSystem.FileSystem,
|
|
209
|
+
FileSystem.FileSystem.of({
|
|
210
|
+
...fs,
|
|
211
|
+
readDirectory: (directory) => {
|
|
212
|
+
reads.push(directory)
|
|
213
|
+
return fs.readDirectory(directory)
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
assert.deepStrictEqual(skills, yield* discover(roots))
|
|
219
|
+
if (mode !== "missing skills")
|
|
220
|
+
assert.deepStrictEqual(reads, [
|
|
221
|
+
path.join(roots.project, ".agents", "skills"),
|
|
222
|
+
path.join(roots.home, ".agents", "skills"),
|
|
223
|
+
])
|
|
224
|
+
}),
|
|
225
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
it.effect("only reads HERMES_HOME/skills/<dir>/SKILL.md", () =>
|
|
230
|
+
withRoots(
|
|
231
|
+
Effect.fnUntraced(function* (roots) {
|
|
232
|
+
yield* writeHermesSkill(
|
|
233
|
+
roots.hermes,
|
|
234
|
+
"outer/inner",
|
|
235
|
+
skillFile("nested", "Ignored"),
|
|
236
|
+
)
|
|
237
|
+
yield* writeHermesSkill(roots.hermes, "", skillFile("bare", "Ignored"))
|
|
238
|
+
yield* writeSkill(
|
|
239
|
+
roots.hermes,
|
|
240
|
+
"agents",
|
|
241
|
+
skillFile("agents", "Ignored"),
|
|
242
|
+
)
|
|
243
|
+
const location = yield* writeHermesSkill(
|
|
244
|
+
roots.hermes,
|
|
245
|
+
"real",
|
|
246
|
+
skillFile("real", "Found"),
|
|
247
|
+
)
|
|
248
|
+
const skills = yield* discoverWithHermes(roots)
|
|
249
|
+
assert.deepStrictEqual(
|
|
250
|
+
skills.map((skill) => [skill.name, skill.location]),
|
|
251
|
+
[["real", location]],
|
|
252
|
+
)
|
|
253
|
+
}),
|
|
254
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
it.effect(
|
|
258
|
+
"makeLocal exposes HERMES_HOME skills and reads their relative references",
|
|
259
|
+
() =>
|
|
260
|
+
withRoots(
|
|
261
|
+
Effect.fnUntraced(function* (roots) {
|
|
262
|
+
const fs = yield* FileSystem.FileSystem
|
|
263
|
+
const path = yield* Path.Path
|
|
264
|
+
const content =
|
|
265
|
+
skillFile("bound", "Workspace skill") +
|
|
266
|
+
"Read references/guide.md.\n"
|
|
267
|
+
const location = yield* writeHermesSkill(
|
|
268
|
+
roots.hermes,
|
|
269
|
+
"bound",
|
|
270
|
+
content,
|
|
271
|
+
)
|
|
272
|
+
const reference = path.join(
|
|
273
|
+
path.dirname(location),
|
|
274
|
+
"references",
|
|
275
|
+
"guide.md",
|
|
276
|
+
)
|
|
277
|
+
yield* fs.makeDirectory(path.dirname(reference))
|
|
278
|
+
yield* fs.writeFileString(
|
|
279
|
+
reference,
|
|
280
|
+
"Workspace reference instructions.",
|
|
281
|
+
)
|
|
282
|
+
yield* withLocalExecutor(
|
|
283
|
+
roots,
|
|
284
|
+
roots.hermes,
|
|
285
|
+
Effect.gen(function* () {
|
|
286
|
+
const executor = yield* AgentExecutor.AgentExecutor
|
|
287
|
+
const capabilities = yield* executor.capabilities
|
|
288
|
+
assert.deepStrictEqual(
|
|
289
|
+
capabilities.skills.map((skill) => [
|
|
290
|
+
skill.name,
|
|
291
|
+
skill.location,
|
|
292
|
+
]),
|
|
293
|
+
[["bound", location]],
|
|
294
|
+
)
|
|
295
|
+
const discovered = capabilities.skills[0]!.location
|
|
296
|
+
const body = yield* executor.executeUnsafe({
|
|
297
|
+
tool: "readFile",
|
|
298
|
+
params: { path: discovered },
|
|
299
|
+
})
|
|
300
|
+
assert.include(String(body), "Read references/guide.md.")
|
|
301
|
+
const guide = yield* executor.executeUnsafe({
|
|
302
|
+
tool: "readFile",
|
|
303
|
+
params: {
|
|
304
|
+
path: path.resolve(
|
|
305
|
+
path.dirname(discovered),
|
|
306
|
+
"references/guide.md",
|
|
307
|
+
),
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
assert.include(String(guide), "Workspace reference instructions.")
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
}),
|
|
314
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
for (const hermesHome of [undefined, ""]) {
|
|
318
|
+
it.effect(
|
|
319
|
+
hermesHome === undefined
|
|
320
|
+
? "makeLocal leaves unset HERMES_HOME disabled"
|
|
321
|
+
: "makeLocal treats empty HERMES_HOME as unset",
|
|
322
|
+
() =>
|
|
323
|
+
withRoots(
|
|
324
|
+
Effect.fnUntraced(function* (roots) {
|
|
325
|
+
const path = yield* Path.Path
|
|
326
|
+
yield* writeHermesSkill(
|
|
327
|
+
path.join(roots.home, ".hermes"),
|
|
328
|
+
"ignored",
|
|
329
|
+
skillFile("ignored", "Ignored"),
|
|
330
|
+
)
|
|
331
|
+
const location = yield* writeSkill(
|
|
332
|
+
roots.project,
|
|
333
|
+
"proj",
|
|
334
|
+
skillFile("proj", "Project"),
|
|
335
|
+
)
|
|
336
|
+
const capabilities = yield* withLocalExecutor(
|
|
337
|
+
roots,
|
|
338
|
+
hermesHome,
|
|
339
|
+
AgentExecutor.AgentExecutor.pipe(
|
|
340
|
+
Effect.flatMap((executor) => executor.capabilities),
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
assert.deepStrictEqual(
|
|
344
|
+
capabilities.skills.map((skill) => skill.location),
|
|
345
|
+
[location],
|
|
346
|
+
)
|
|
347
|
+
}),
|
|
348
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
65
352
|
for (const type of ["FIFO", "Socket", "Directory"] as const) {
|
|
66
353
|
it.effect(`skips a ${type} SKILL.md without attempting to read it`, () =>
|
|
67
354
|
withRoots(
|
|
@@ -456,7 +743,7 @@ body`,
|
|
|
456
743
|
it.effect("returns an empty catalog when nothing exists", () =>
|
|
457
744
|
withRoots(
|
|
458
745
|
Effect.fnUntraced(function* (roots) {
|
|
459
|
-
const skills = yield*
|
|
746
|
+
const skills = yield* discoverWithHermes(roots, Option.none())
|
|
460
747
|
assert.deepStrictEqual(skills, [])
|
|
461
748
|
assert.isTrue(Option.isNone(AgentSkills.renderCatalog(skills)))
|
|
462
749
|
}),
|
|
@@ -478,19 +765,11 @@ body`,
|
|
|
478
765
|
)
|
|
479
766
|
yield* writeSkill(roots.project, "broken", "---\nnope\n---")
|
|
480
767
|
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
AgentExecutor.layerLocal({ directory: roots.project }).pipe(
|
|
487
|
-
Layer.provide(NodeHttpClient.layerUndici),
|
|
488
|
-
),
|
|
489
|
-
),
|
|
490
|
-
Effect.ensuring(
|
|
491
|
-
Effect.sync(() => {
|
|
492
|
-
process.env.HOME = previousHome
|
|
493
|
-
}),
|
|
768
|
+
const capabilities = yield* withLocalExecutor(
|
|
769
|
+
roots,
|
|
770
|
+
undefined,
|
|
771
|
+
AgentExecutor.AgentExecutor.pipe(
|
|
772
|
+
Effect.flatMap((executor) => executor.capabilities),
|
|
494
773
|
),
|
|
495
774
|
)
|
|
496
775
|
assert.deepStrictEqual(
|
|
@@ -567,6 +846,48 @@ const systemPromptFor = (skills: ReadonlyArray<AgentSkills.Skill>) =>
|
|
|
567
846
|
)
|
|
568
847
|
|
|
569
848
|
describe("Agent skills catalog", () => {
|
|
849
|
+
it.effect(
|
|
850
|
+
"renders a Hermes skill in the system prompt with its absolute location",
|
|
851
|
+
() =>
|
|
852
|
+
withRoots(
|
|
853
|
+
Effect.fnUntraced(function* (roots) {
|
|
854
|
+
const location = yield* writeHermesSkill(
|
|
855
|
+
roots.hermes,
|
|
856
|
+
"bound",
|
|
857
|
+
skillFile("bound", "Workspace skill"),
|
|
858
|
+
)
|
|
859
|
+
const system = yield* systemPromptFor(
|
|
860
|
+
yield* discoverWithHermes(roots),
|
|
861
|
+
)
|
|
862
|
+
assert.include(system, "# Skills")
|
|
863
|
+
assert.include(system, "- bound: Workspace skill")
|
|
864
|
+
assert.include(system, "location: " + location)
|
|
865
|
+
assert.notInclude(system, "Instructions for bound.")
|
|
866
|
+
}),
|
|
867
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
it.effect(
|
|
871
|
+
"omits the catalog with HERMES_HOME pointing at an empty directory",
|
|
872
|
+
() =>
|
|
873
|
+
withRoots(
|
|
874
|
+
Effect.fnUntraced(function* (roots) {
|
|
875
|
+
const capabilities = yield* withLocalExecutor(
|
|
876
|
+
roots,
|
|
877
|
+
roots.hermes,
|
|
878
|
+
AgentExecutor.AgentExecutor.pipe(
|
|
879
|
+
Effect.flatMap((executor) => executor.capabilities),
|
|
880
|
+
),
|
|
881
|
+
)
|
|
882
|
+
assert.deepStrictEqual(capabilities.skills, [])
|
|
883
|
+
assert.notInclude(
|
|
884
|
+
yield* systemPromptFor(capabilities.skills),
|
|
885
|
+
"# Skills",
|
|
886
|
+
)
|
|
887
|
+
}),
|
|
888
|
+
).pipe(Effect.provide(NodeServices.layer)),
|
|
889
|
+
)
|
|
890
|
+
|
|
570
891
|
it.effect(
|
|
571
892
|
"escapes a newline in a catalog location without changing the filesystem path",
|
|
572
893
|
() =>
|
package/src/AgentSkills.ts
CHANGED
|
@@ -37,9 +37,11 @@ export class Skill extends Schema.Class<Skill>("Skill")({
|
|
|
37
37
|
}) {}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Scan project and user `.agents/skills/<dir>/SKILL.md` files
|
|
40
|
+
* Scan project and user `.agents/skills/<dir>/SKILL.md` files, plus
|
|
41
|
+
* `<hermesHome>/skills/<dir>/SKILL.md` when explicitly configured.
|
|
41
42
|
*
|
|
42
|
-
* Project names override
|
|
43
|
+
* Project names override Hermes names, which override user names.
|
|
44
|
+
* The first name wins within each scope; an empty Hermes home is ignored.
|
|
43
45
|
* Skip unreadable files, invalid frontmatter and missing or empty descriptions.
|
|
44
46
|
*
|
|
45
47
|
* @since 1.0.0
|
|
@@ -48,6 +50,7 @@ export class Skill extends Schema.Class<Skill>("Skill")({
|
|
|
48
50
|
export const discover: (options: {
|
|
49
51
|
readonly directory: string
|
|
50
52
|
readonly homeDirectory: Option.Option<string>
|
|
53
|
+
readonly hermesHome?: Option.Option<string>
|
|
51
54
|
}) => Effect.Effect<
|
|
52
55
|
ReadonlyArray<Skill>,
|
|
53
56
|
never,
|
|
@@ -57,14 +60,31 @@ export const discover: (options: {
|
|
|
57
60
|
const path = yield* Path.Path
|
|
58
61
|
|
|
59
62
|
const roots: Array<{ readonly root: string; readonly source: SkillSource }> =
|
|
60
|
-
[
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
root: path.resolve(options.directory, ".agents", "skills"),
|
|
66
|
+
source: "project",
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
if (
|
|
70
|
+
options.hermesHome &&
|
|
71
|
+
Option.isSome(options.hermesHome) &&
|
|
72
|
+
options.hermesHome.value !== ""
|
|
73
|
+
) {
|
|
74
|
+
roots.push({
|
|
75
|
+
root: path.resolve(options.hermesHome.value, "skills"),
|
|
76
|
+
source: "user",
|
|
77
|
+
})
|
|
78
|
+
}
|
|
61
79
|
if (Option.isSome(options.homeDirectory)) {
|
|
62
|
-
roots.push({
|
|
80
|
+
roots.push({
|
|
81
|
+
root: path.resolve(options.homeDirectory.value, ".agents", "skills"),
|
|
82
|
+
source: "user",
|
|
83
|
+
})
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
const byName = new Map<string, Skill>()
|
|
66
|
-
for (const { root, source } of roots) {
|
|
67
|
-
const skillsDir = path.resolve(root, ".agents", "skills")
|
|
87
|
+
for (const { root: skillsDir, source } of roots) {
|
|
68
88
|
const entries = yield* fs
|
|
69
89
|
.readDirectory(skillsDir)
|
|
70
90
|
.pipe(Effect.orElseSucceed(() => []))
|