clanka 0.4.0 → 0.5.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.
@@ -10,6 +10,7 @@ import {
10
10
  AgentTools,
11
11
  AgentToolsWithSearch,
12
12
  CurrentDirectory,
13
+ DirectoryChanger,
13
14
  SubagentExecutor,
14
15
  TaskCompleter,
15
16
  } from "./AgentTools.ts"
@@ -100,12 +101,22 @@ export const makeLocal = Effect.fnUntraced(function* <
100
101
  Toolkit extends Toolkit.Toolkit<infer T>
101
102
  ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
102
103
  : never,
103
- CurrentDirectory | SubagentExecutor | TaskCompleter
104
+ CurrentDirectory | DirectoryChanger | SubagentExecutor | TaskCompleter
104
105
  >
105
106
  > {
106
107
  const fs = yield* FileSystem.FileSystem
107
108
  const pathService = yield* Path.Path
108
109
  const renderer = yield* ToolkitRenderer
110
+ let currentDirectory = pathService.resolve(options.directory)
111
+ const changeDirectory = Effect.fnUntraced(function* (directory: string) {
112
+ const target = pathService.resolve(currentDirectory, directory)
113
+ const info = yield* fs.stat(target)
114
+ if (info.type !== "Directory") {
115
+ return yield* Effect.die(new Error(`Not a directory: ${target}`))
116
+ }
117
+ currentDirectory = target
118
+ return target
119
+ }, Effect.orDie)
109
120
  const search = yield* Effect.serviceOption(SemanticSearch)
110
121
  const hasSearch = Option.isSome(search)
111
122
  const AllTools = Toolkit.merge(
@@ -142,7 +153,7 @@ export const makeLocal = Effect.fnUntraced(function* <
142
153
 
143
154
  const taskServices = Context.empty().pipe(
144
155
  Context.add(TaskCompleter, opts.onTaskComplete),
145
- Context.add(CurrentDirectory, options.directory),
156
+ Context.add(DirectoryChanger, changeDirectory),
146
157
  Context.add(SubagentExecutor, opts.onSubagent),
147
158
  Context.add(Console.Console, console),
148
159
  Context.add(References.CurrentLogAnnotations, {}),
@@ -165,12 +176,13 @@ export const makeLocal = Effect.fnUntraced(function* <
165
176
 
166
177
  for (let i = 0; i < toolEntries.length; i++) {
167
178
  const { name, handler, services } = toolEntries[i]!
168
- const runFork = Effect.runForkWith(
169
- Context.merge(services, taskServices),
170
- )
179
+ const toolServices = Context.merge(services, taskServices)
171
180
 
172
181
  // oxlint-disable-next-line typescript/no-explicit-any
173
182
  sandbox[name] = function (params: any) {
183
+ const runFork = Effect.runForkWith(
184
+ Context.add(toolServices, CurrentDirectory, currentDirectory),
185
+ )
174
186
  running++
175
187
  const fiber = trackFiber(runFork(handler(params, {})))
176
188
  return new Promise((resolve, reject) => {
@@ -213,6 +225,7 @@ export const makeLocal = Effect.fnUntraced(function* <
213
225
  const skills = yield* AgentSkills.discover({
214
226
  directory: options.directory,
215
227
  homeDirectory: homeDirectory(),
228
+ hermesHome: Option.fromNullishOr(process.env.HERMES_HOME),
216
229
  }).pipe(
217
230
  Effect.provideService(FileSystem.FileSystem, fs),
218
231
  Effect.provideService(Path.Path, pathService),
@@ -225,33 +238,38 @@ export const makeLocal = Effect.fnUntraced(function* <
225
238
  })
226
239
  }),
227
240
  execute,
228
- executeUnsafe: (opts) => {
229
- const tool = tools.tools[opts.tool as keyof typeof tools.tools]
230
- const handler = services.mapUnsafe.get(tool.id) as Tool.Handler<string>
231
- if (!handler || !tool) {
232
- return Effect.die(new Error(`Unknown tool: ${opts.tool}`))
233
- }
241
+ executeUnsafe: (opts) =>
242
+ Effect.suspend(() => {
243
+ const tool = tools.tools[opts.tool as keyof typeof tools.tools]
244
+ if (!tool) {
245
+ return Effect.die(new Error(`Unknown tool: ${opts.tool}`))
246
+ }
247
+ const handler = services.mapUnsafe.get(tool.id) as Tool.Handler<string>
248
+ if (!handler) {
249
+ return Effect.die(new Error(`Unknown tool: ${opts.tool}`))
250
+ }
234
251
 
235
- const taskServices = handler.context.pipe(
236
- Context.add(TaskCompleter, () => Effect.void),
237
- Context.add(CurrentDirectory, options.directory),
238
- Context.add(SubagentExecutor, () => Effect.succeed("")),
239
- Context.add(References.CurrentLogAnnotations, {}),
240
- Option.isSome(search)
241
- ? Context.add(SemanticSearch, search.value)
242
- : identity,
243
- )
252
+ const taskServices = handler.context.pipe(
253
+ Context.add(TaskCompleter, () => Effect.void),
254
+ Context.add(CurrentDirectory, currentDirectory),
255
+ Context.add(DirectoryChanger, changeDirectory),
256
+ Context.add(SubagentExecutor, () => Effect.succeed("")),
257
+ Context.add(References.CurrentLogAnnotations, {}),
258
+ Option.isSome(search)
259
+ ? Context.add(SemanticSearch, search.value)
260
+ : identity,
261
+ )
244
262
 
245
- const encodeSuccess = Schema.encodeUnknownEffect(
246
- Schema.toCodecJson(tool.successSchema as Schema.Top),
247
- )
248
- return pipe(
249
- handler.handler(opts.params, {}),
250
- Effect.flatMap(encodeSuccess),
251
- Effect.provideContext(taskServices),
252
- Effect.orDie,
253
- ) as Effect.Effect<Schema.Json>
254
- },
263
+ const encodeSuccess = Schema.encodeUnknownEffect(
264
+ Schema.toCodecJson(tool.successSchema as Schema.Top),
265
+ )
266
+ return pipe(
267
+ handler.handler(opts.params, {}),
268
+ Effect.flatMap(encodeSuccess),
269
+ Effect.provideContext(taskServices),
270
+ Effect.orDie,
271
+ ) as Effect.Effect<Schema.Json>
272
+ }),
255
273
  })
256
274
  })
257
275
 
@@ -319,7 +337,7 @@ export const layerLocal = <Toolkit extends Toolkit.Any = never>(options: {
319
337
  Toolkit extends Toolkit.Toolkit<infer T>
320
338
  ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
321
339
  : never,
322
- CurrentDirectory | SubagentExecutor | TaskCompleter
340
+ CurrentDirectory | DirectoryChanger | SubagentExecutor | TaskCompleter
323
341
  >
324
342
  > =>
325
343
  Layer.effect(AgentExecutor, makeLocal(options)).pipe(
@@ -359,7 +377,7 @@ export const layerRpcServer = <Toolkit extends Toolkit.Any = never>(options: {
359
377
  Toolkit extends Toolkit.Toolkit<infer T>
360
378
  ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
361
379
  : never,
362
- CurrentDirectory | SubagentExecutor | TaskCompleter
380
+ CurrentDirectory | DirectoryChanger | SubagentExecutor | TaskCompleter
363
381
  >
364
382
  > =>
365
383
  RpcServer.layer(Rpcs, {
@@ -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* discover(roots)
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 previousHome = process.env.HOME
482
- process.env.HOME = roots.home
483
- const capabilities = yield* AgentExecutor.AgentExecutor.pipe(
484
- Effect.flatMap((executor) => executor.capabilities),
485
- Effect.provide(
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
  () =>
@@ -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 user names; the first name wins within each scope.
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
- [{ root: options.directory, source: "project" }]
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({ root: options.homeDirectory.value, source: "user" })
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(() => []))