{"record":{"id":"309e1b3c15b5cf2e","repo":"larksuite/cli","slug":"s-q-is-not-a-directory-every-s-entry-must-be","errorCode":null,"errorMessage":"%s: %q is not a directory; every %s entry must be a <skill>/ dir","messagePattern":"(.+?): %q is not a directory; every (.+?) entry must be a <skill>/ dir","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/skillpolicy/resolver.go","lineNumber":178,"sourceCode":"\tskills map[string]skillManifest\n}\n\n// scanSkillTree validates and snapshots a skill tree's top level in one\n// pass. The returned set is the only source used by validation and overlay\n// composition, so a mutable FS cannot swap unvalidated names between phases.\nfunc scanSkillTree(label string, source fs.FS) (skillTreeSnapshot, error) {\n\tsnapshot := skillTreeSnapshot{source: source, skills: map[string]skillManifest{}}\n\tif source == nil {\n\t\treturn snapshot, nil\n\t}\n\tentries, err := fs.ReadDir(source, \".\")\n\tif err != nil {\n\t\treturn snapshot, fmt.Errorf(\"%s: cannot read root: %w\", label, err)\n\t}\n\tfor _, e := range entries {\n\t\tname := e.Name()\n\t\tif !e.IsDir() {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: %q is not a directory; every %s entry must be a <skill>/ dir\", label, name, label)\n\t\t}\n\t\tif !isSkillName(name) {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: %q is not a valid skill name\", label, name)\n\t\t}\n\t\tok, err := skillExists(source, name)\n\t\tif err != nil {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: probing skill %q: %w\", label, name, err)\n\t\t}\n\t\tif !ok {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: skill %q is missing SKILL.md\", label, name)\n\t\t}\n\t\tmanifest, err := readSkillManifest(source, name)\n\t\tif err != nil {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: skill %q has invalid metadata: %w\", label, name, err)\n\t\t}\n\t\tsnapshot.skills[name] = manifest\n\t}\n\treturn snapshot, nil","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/skillpolicy/resolver.go#L160-L196","documentation":"A top-level entry of the skill tree is not a directory; every entry must be a <skill>/ directory. scanSkillTree rejects flat files in the tree root so plugins cannot smuggle non-skill content into the composed FS.","triggerScenarios":"ResolveWithReferences with a Base or Overlay FS whose root contains a regular file (README.md, LICENSE, .DS_Store, dotfile) — the FS walks entries and fails on the first non-directory.","commonSituations":"go:embed patterns that include files alongside skill directories, macOS metadata files committed into embedded assets, or a plugin author adding notes at the overlay root.","solutions":["Remove the offending file (named in the message) from the embedded tree root","Adjust the embed pattern to include only skill directories","Add a build/test check that lists the FS root and asserts every entry IsDir","Keep non-skill assets in a separate FS outside the skill tree"],"exampleFix":"// before\n//go:embed overlay/*\nvar overlayFS embed.FS // includes overlay/NOTES.md\n// after\n//go:embed overlay\nvar embedded embed.FS // remove overlay/NOTES.md first","handlingStrategy":"validation","validationCode":"entries, _ := fs.ReadDir(skillFS, \".\")\nfor _, e := range entries {\n\tif !e.IsDir() { return fmt.Errorf(\"%q must be a directory\", e.Name()) }\n}","typeGuard":null,"tryCatchPattern":"_, err := skillpolicy.ResolveWithReferences(base, specs)\nif err != nil && strings.Contains(err.Error(), \"is not a directory\") { /* remove flat file */ }","preventionTips":["Keep only skill directories at the tree root","Scope embed patterns to directories","Gitignore OS metadata files (.DS_Store)","Add a tree-shape unit test"],"tags":["go","fs","skill-tree","validation"],"backgroundTag":"invalid-skill-tree-layout","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}