From eed6113359ed944b67a3a268d54b76f7a01cfa1a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 19:54:06 +0100 Subject: [PATCH] refactor(skills): stabilize watcher targets and include agents skills --- src/agents/skills/refresh.test.ts | 10 ++++++++-- src/agents/skills/refresh.ts | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/agents/skills/refresh.test.ts b/src/agents/skills/refresh.test.ts index 7d81e09ad..64701c3ec 100644 --- a/src/agents/skills/refresh.test.ts +++ b/src/agents/skills/refresh.test.ts @@ -1,3 +1,4 @@ +import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; @@ -22,10 +23,15 @@ describe("ensureSkillsWatcher", () => { const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown }; expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED); + const posix = (p: string) => p.replaceAll("\\", "/"); expect(targets).toEqual( expect.arrayContaining([ - path.join("/tmp/workspace", "skills", "SKILL.md"), - path.join("/tmp/workspace", "skills", "*", "SKILL.md"), + posix(path.join("/tmp/workspace", "skills", "SKILL.md")), + posix(path.join("/tmp/workspace", "skills", "*", "SKILL.md")), + posix(path.join("/tmp/workspace", ".agents", "skills", "SKILL.md")), + posix(path.join("/tmp/workspace", ".agents", "skills", "*", "SKILL.md")), + posix(path.join(os.homedir(), ".agents", "skills", "SKILL.md")), + posix(path.join(os.homedir(), ".agents", "skills", "*", "SKILL.md")), ]), ); expect(targets.every((target) => target.includes("SKILL.md"))).toBe(true); diff --git a/src/agents/skills/refresh.ts b/src/agents/skills/refresh.ts index 85402024a..a9f92f2be 100644 --- a/src/agents/skills/refresh.ts +++ b/src/agents/skills/refresh.ts @@ -1,4 +1,5 @@ import chokidar, { type FSWatcher } from "chokidar"; +import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "../../config/config.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; @@ -59,8 +60,10 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin const paths: string[] = []; if (workspaceDir.trim()) { paths.push(path.join(workspaceDir, "skills")); + paths.push(path.join(workspaceDir, ".agents", "skills")); } paths.push(path.join(CONFIG_DIR, "skills")); + paths.push(path.join(os.homedir(), ".agents", "skills")); const extraDirsRaw = config?.skills?.load?.extraDirs ?? []; const extraDirs = extraDirsRaw .map((d) => (typeof d === "string" ? d.trim() : "")) @@ -72,17 +75,24 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin return paths; } +function toWatchGlobRoot(raw: string): string { + // Chokidar treats globs as POSIX-ish patterns. Normalize Windows separators + // so `*` works consistently across platforms. + return raw.replaceAll("\\", "/").replace(/\/+$/, ""); +} + function resolveWatchTargets(workspaceDir: string, config?: OpenClawConfig): string[] { // Skills are defined by SKILL.md; watch only those files to avoid traversing // or watching unrelated large trees (e.g. datasets) that can exhaust FDs. const targets = new Set(); for (const root of resolveWatchPaths(workspaceDir, config)) { + const globRoot = toWatchGlobRoot(root); // Some configs point directly at a skill folder. - targets.add(path.join(root, "SKILL.md")); + targets.add(`${globRoot}/SKILL.md`); // Standard layout: //SKILL.md - targets.add(path.join(root, "*", "SKILL.md")); + targets.add(`${globRoot}/*/SKILL.md`); } - return Array.from(targets); + return Array.from(targets).toSorted(); } export function registerSkillsChangeListener(listener: (event: SkillsChangeEvent) => void) {