From d361a3311470b91c9d9184a968ed13c56e74f6cb Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 28 Dec 2025 09:22:03 -0700 Subject: [PATCH] refactor(skills): use fastwalk to resolve symlinks (#1732) --- internal/skills/skills.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/skills/skills.go b/internal/skills/skills.go index 384f589d423b0855b27c985f0914049e17135393..cba0b994d188e184fdcb55cf98bd080764e34327 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -10,7 +10,9 @@ import ( "path/filepath" "regexp" "strings" + "sync" + "github.com/charlievieth/fastwalk" "gopkg.in/yaml.v3" ) @@ -110,17 +112,32 @@ func splitFrontmatter(content string) (frontmatter, body string, err error) { // Discover finds all valid skills in the given paths. func Discover(paths []string) []*Skill { var skills []*Skill + var mu sync.Mutex seen := make(map[string]bool) for _, base := range paths { - filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error { + // We use fastwalk with Follow: true instead of filepath.WalkDir because + // WalkDir doesn't follow symlinked directories at any depth—only entry + // points. This ensures skills in symlinked subdirectories are discovered. + // fastwalk is concurrent, so we protect shared state (seen, skills) with mu. + conf := fastwalk.Config{ + Follow: true, + ToSlash: fastwalk.DefaultToSlash(), + } + fastwalk.Walk(&conf, base, func(path string, d os.DirEntry, err error) error { if err != nil { return nil } - if d.IsDir() || d.Name() != SkillFileName || seen[path] { + if d.IsDir() || d.Name() != SkillFileName { + return nil + } + mu.Lock() + if seen[path] { + mu.Unlock() return nil } seen[path] = true + mu.Unlock() skill, err := Parse(path) if err != nil { slog.Warn("Failed to parse skill file", "path", path, "error", err) @@ -131,7 +148,9 @@ func Discover(paths []string) []*Skill { return nil } slog.Info("Successfully loaded skill", "name", skill.Name, "path", path) + mu.Lock() skills = append(skills, skill) + mu.Unlock() return nil }) }